本节书摘来异步社区《Java遗传算法编程》一书中的第2章,第2.8节,作者: 【英】Lee Jacobson(雅各布森) , 【美】Burak Kanber(坎贝尔),更多章节内容可以访问云栖社区“异步社区”公众号查看。
2.8 交叉实现
为了实现轮盘赌选择,在GeneticAlgorithm类的任意位置增加一个selectParent( )方法。
public Individual selectParent(Population population) {
// Get individuals
Individual individuals[] = population.getIndividuals();
// Spin roulette wheel
double populationFitness = population.getPopulationFitness();
double rouletteWheelPosition = Math.random() * populationFitness;
// Find parent
double spinWheel = 0;
for (Individual individual : individuals) {
spinWheel += individual.getFitness();
if (spinWheel >= rouletteWheelPosition) {
return individual;
}
}
return individuals[population.size() - 1];
}
selectParent( )方法基本上是反着玩轮盘赌。在赌场,轮盘上已经有标记,然后旋转的轮盘,等待球落入位置。但在这里,我们先选择一个随机位置,然后反向工作,弄清楚哪个个体位于该位置。在算法上,这样更容易实现。选择一个介于0和种群总适应度的随机数,然后逐个检查每个个体,同时累加它们的适应度值,直到达到起初选择的随机位置。
既然已经添加了选择方法,下一步就是创建一个交叉方法,利用这个selectParent()方法来选择交叉伙伴。首先,将下面的交叉方法添加到GeneticAlgorithm类。
public Population crossoverPopulation(Population population) {
// Create new population
Population newPopulation = new Population(population.size());
// Loop over current population by fitness
for (int populationIndex = 0; populationIndex < population.size();
populationIndex++) {
Individual parent1 = population.getFittest(populationIndex);
// Apply crossover to this individual?
if (this.crossoverRate > Math.random() && populationIndex >
this.elitismCount) {
// Initialize offspring
Individual offspring = new Individual(parent1.
getChromosomeLength());
// Find second parent
Individual parent2 = selectParent(population);
// Loop over genome
for (int geneIndex = 0; geneIndex < parent1.
getChromosomeLength(); geneIndex++) {
// Use half of parent1's genes and half of
parent2's genes
if (0.5 > Math.random()) {
offspring.setGene(geneIndex,
parent1.getGene(geneIndex));
} else {
offspring.setGene(geneIndex,
parent2.getGene(geneIndex));
}
}
// Add offspring to new population
newPopulation.setIndividual(populationIndex,
offspring);
} else {
// Add individual to new population without applying
crossover
newPopulation.setIndividual
(populationIndex, parent1);
}
}
return newPopulation;
}
在crossoverPopulation()方法的第一行,为下一代创建一个新的空种群。接下来,遍历种群,利用交叉率来考虑每个个体的交叉(这里还有一个神秘的术语“精英主义”,我们将在下一节讨论)。如果个体不经过交叉,它就直接加入下一个种群,否则就创建一个新个体。后代染色体的填充方法是遍历亲代染色体,随机从每个亲代选择基因,加入后代的染色体。针对种群的每个个体完成这个交叉过程后,交叉方法返回下一代的种群。
至此,我们可在以AllOnesGA类的main方法中,实现交叉的功能。整个AllOnesGA类和main方法打印如下,但和以前比,唯一的变化是在“Apply crossover”注释下面,加入一行crossoverPopulation()调用。
package chapter2;
public class AllOnesGA {
public static void main(String[] args) {
// Create GA object
GeneticAlgorithm ga = new GeneticAlgorithm(100, 0.001, 0.95, 0);
// Initialize population
Population population = ga.initPopulation(50);
// Evaluate population
ga.evalPopulation(population);
// Keep track of current generation
int generation = 1;
while (ga.isTerminationConditionMet(population) == false) {
// Print fittest individual from population
System.out.println("Best solution: " + population.
getFittest(0).toString());
// Apply crossover
population = ga.crossoverPopulation(population);
// Apply mutation
// TODO
// Evaluate population
ga.evalPopulation(population);
// Increment the current generation
generation++;
}
System.out.println("Found solution in " + generation + "
generations");
System.out.println("Best solution: " + population.
getFittest(0).toString());
}
}
现在,运行程序应该能工作,并返回一个有效的解!你可以尝试一下,点击Eclipse中的Run按钮,观察控制台中的结果。
正如你所看到的,单独的交叉足以进化种群。然而,如果没有变异,遗传算法很容易陷入局部最优而不能找到全局最优。在这样简单的问题中,我们不会看到这种现象,但在更复杂的问题领域,我们需要某种机制,将一个种群推离局部最优,试试看是否有其他更好的解。这就是变异的随机性发挥作用的地方:如果一个解停滞在局部最优,随机事件可能将它踢向正确的方向,让它朝着更好的解前进。
2.8.1 精英主义
在讨论变异之前,先来看看我们在交叉方法中引入的elitismCount参数。
由于交叉和变异算子的效果,基本遗传算法往往会在世代之间失去种群中最好的个体。然而,我们需要这些算子来找到更好的解。要在实战中看到这个问题,只需编辑遗传算法的代码,针对每一代打印出最适应个体的适应度。你会发现,尽管它通常会增长,有时候在交叉和变异时最优解会丢失,代之以非最优解。
解决这个问题的一个简单的优化技术,就是始终让最适合的个体不作改变,添加到下一代的种群中。这样,最优个体不会在世代之间丢失。虽然这些个体没有进行交叉操作,但它们仍会被选为另一个个体的亲代,让它们的遗传信息仍然可以和种群中的其他个体分享。将最优解保留到下一代的过程称为“精英主义”。
通常情况下,群体中“精英”个体的最佳数目只占种群总规模的很小一部分。原因是如果该值太高,就会减缓遗传算法搜索过程,因为保留太多个体导致缺乏遗传多样性。和以前讨论的其他参数类似,为最佳表现找到一种平衡,这是非常重要的。
实现精英主义在交叉和变异中都很简单。让我们重新检查crossover Population( ),看看是否应该应用交叉:
// Apply crossover to this individual?
if (this.crossoverRate > Math.random() && populationIndex >= this.
elitismCount) {
// ...
}
仅当交叉条件满足且个体不是精英,才交叉。
个体如何才算精英?此时,种群中的个体已经按它们的适应度排序,因此最强大的个体索引值最小。因此,如果我们需要3个精英个体,应该跳过索引0~2。这将保留最强大的个体,让它们保持不变,传递到下一代。在接下来的变异代码中,我们将使用完全相同的条件。
2.8.2 变异
要完成进化过程,最后要添加的是变异。像交叉一样,有许多不同的变异方法可供选择。如果使用二进制串,一种比较常见的方法是所谓的“位翻转”变异。你可能已经猜到,位翻转变异就是根据位的初始值,将它从1翻转为0,或从0翻转到1。如果染色体使用另外某种形式编码,通常会采用不同的变异方法,以便更好地利用这种编码。
在选择变异和交叉方法时,有一点非常重要,即确保你选择的方法仍然得到一个有效解。我们将后续章节中看到这一概念的实战,但对于这个问题,我们只需要确保基因变异只能产生0和1。例如,基因变异为7将给出一个无效的解。
在本章中,这个建议似乎没有实际意义,而且过于明显,但请考虑另一个简单的问题,如果你需要不重复地从1到6排序(即得到“123456”)。变异算法如果简单地选择1到6之间的随机数,可能产生“126456”,使用“6”两次,这将是一个无效的解,因为每个数字只能用一次。正如你所看到的,即使简单的问题,有时也需要复杂的技术。
与交叉类似,变异基于变异率应用于个体。如果变异率设定为0.1,那么每个基因在变异阶段发生变异的机会是10%。
让我们继续,为GeneticAlgorithm类添加变异的功能。可以将它添加到任何位置:
public Population mutatePopulation(Population population) {
// Initialize new population
Population newPopulation = new Population(this.populationSize);
// Loop over current population by fitness
for (int populationIndex = 0; populationIndex < population.size();
populationIndex++) {
Individual individual = population.
getFittest(populationIndex);
// Loop over individual's genes
for (int geneIndex = 0; geneIndex < individual.
getChromosomeLength(); geneIndex++) {
// Skip mutation if this is an elite individual
if (populationIndex >= this.elitismCount) {
// Does this gene need mutation?
if (this.mutationRate > Math.random()) {
// Get new gene
int newGene = 1;
if (individual.getGene(geneIndex) == 1) {
newGene = 0;
}
// Mutate gene
individual.setGene(geneIndex, newGene);
}
}
}
// Add individual to population
newPopulation.setIndividual(populationIndex, individual);
}
// Return mutated population
return newPopulation;
}
mutatePopulation( )方法开始为变异的个体创建一个新的空种群,然后开始遍历当前种群。然后,遍历每个个体的染色体,基于变异率,考虑每个基因是否进行位翻转变异。个体的整个染色体遍历完成后,该个体就被加入新的变异种群。所有的个体都通过变异过程后,返回该变异种群。
现在,我们可以完成进化循环的最后一步,将变异功能加到main方法。完成的main方法如下。与前面相比有两处不同:首先,我们在“Apply mutation”注释的下面添加了mutatePopulation()调用。此外,既然已经明白了精英主义的工作原理,我们在new GeneticAlgorithm构造方法中,将elitismCount参数从0改为2。
package chapter2;
public class AllOnesGA {
public static void main(String[] args) {
// Create GA object
GeneticAlgorithm ga = new GeneticAlgorithm(100, 0.001, 0.95, 2);
// Initialize population
Population population = ga.initPopulation(50);
// Evaluate population
ga.evalPopulation(population);
// Keep track of current generation
int generation = 1;
while (ga.isTerminationConditionMet(population) == false) {
// Print fittest individual from population
System.out.println("Best solution: " + population.
getFittest(0).toString());
// Apply crossover
population = ga.crossoverPopulation(population);
// Apply mutation
population = ga.mutatePopulation(population);
// Evaluate population
ga.evalPopulation(population);
// Increment the current generation
generation++;
}
System.out.println("Found solution in " + generation + "
generations");
System.out.println("Best solution: " + population.
getFittest(0).toString());
}
}
2.8.3 执行
现在,你已经完成了第一个遗传算法。Individual和Population类在本章前面已经完全打印,你的这些类应该看起来就像上面一样。最后的AllOnesGA执行类(引导和运行算法的类)直接在上面提供。
GeneticAlgorithm类相当长,你一块一块地创建了它,所以此时,请检查是否已经实现了以下属性和方法。为了节省篇幅,这里省略了所有的注释和方法体(我只是显示了类的折叠视图),但要确保你的类有这些方法,像前面介绍的一样实现。
package chapter2;
public class GeneticAlgorithm {
private int populationSize;
private double mutationRate;
private double crossoverRate;
private int elitismCount;
public GeneticAlgorithm(int populationSize, double mutationRate, double
crossoverRate, int elitismCount) { }
public Population initPopulation(int chromosomeLength) { }
public double calcFitness(Individual individual) { }
public void evalPopulation(Population population) { }
public boolean isTerminationConditionMet(Population population) { }
public Individual selectParent(Population population) { }
public Population crossoverPopulation(Population population) { }
public Population mutatePopulation(Population population) { }
}
如果你使用Eclipse IDE,现在可以运行该算法,打开AllOnesGA文件,然后点击“Run”按钮,该按钮通常在IDE的顶部菜单中。
在运行时,该算法将信息打印到控制台,点击Run时,控制台会自动出现在Eclipse中。因为每一个遗传算法的随机性,每次运行看起来有点不同,但输出看起来可能如下所示:
Best solution: 11001110100110111111010111001001100111110011111111
Best solution: 11001110100110111111010111001001100111110011111111
Best solution: 11001110100110111111010111001001100111110011111111
[ ... Lots of lines omitted here ... ]
Best solution: 11111111111111111111111111111011111111111111111111
Best solution: 11111111111111111111111111111011111111111111111111
Found solution in 113 generations
Best solution: 11111111111111111111111111111111111111111111111111
此时,你应该尝试给GeneticAlgorithm构造方法提供不同的参数:populationSize、mutationRate、crossoverRate和elitismCount。不要忘记,统计决定了遗传算法的表现,所以不能通过运行一次来评价一个算法或一种设置的表现:每种不同的设置至少要尝试10次,才能判断其表现。