作者: 【英】Lee Jacobson(雅各布森) , 【美】Burak Kanber(坎贝尔)

为了消除所有不必要的细节,保持最初的实现容易尝试,本书中介绍的第一个遗传算法将是简单的二进制遗传算法。

二进制遗传算法比较容易实现,对于解决许多种优化问题,它可能是非常有效的工具。你可能还记得第1章提到,二进制遗传算法是由Holland(1975)提出的原创的遗传算法。

2.4.1 问题

首先,让我们回顾一下“全一”问题,它是可以用二进制遗传算法来解决的一个非常基本的问题。

该问题不是很有趣,但作为一个简单的问题,它的作用是强调所涉及的基本技术。顾名思义,该问题就是发现全部由1构成的字符串。因此,对于长度为5的字符串,最优解是“11111”。

2.4.2 参数

既然有了要解决的问题,让我们继续研究实现。我们要做的第一件事就是建立遗传算法参数。如前所述,这3个主要参数是种群规模、变异率和交叉率。本章中,我们还引入了一个名为“精英主义(elitism)”的概念,并将它作为遗传算法的参数之一。

首先,创建一个名为GeneticAlgorithm的类。如果你使用Eclipse,可以通过选择File New Class来做到这一点。在本书中,我们选择用对应的章名来命名包,所以我们会在包“Chapter2”中工作。

这个GeneticAlgorithm类将包含遗传算法本身的操作所需的方法和变量。例如,这个类包括处理交叉、变异、适应度评估和终止条件检查的逻辑。该类创建后,添加一个构造方法,它接受4个参数:种群规模、变异率、交叉率和精英成员数。

package chapter2;
/**
 * Lots of comments in the source that are omitted here!
 */
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) {
              this.populationSize = populationSize;
              this.mutationRate = mutationRate;
              this.crossoverRate = crossoverRate;
              this.elitismCount = elitismCount;
       }

       /**
        * Many more methods implemented later...
        */
}

传入所需的参数,这个构造方法将利用所需的配置,创建GeneticAlgorithm类的新实例。

现在,我们应该创建引导类:回想一下,每章都需要一个引导类,用于初始化遗传算法,并作为应用程序的起点。将该类命名为“AllOnesGA”,并定义一个main方法:

package chapter2;
public class AllOnesGA {
       public static void main(String[] args) {
              // Create GA object
              GeneticAlgorithm ga = new GeneticAlgorithm(100, 0.01, 0.95, 0);
              // We’ll add a lot more here...
       }
}

暂时,我们就用一些典型的参数值:种群规模=100,变异率=0.01,交叉率=0.95,精英计数为 0(实际上暂且禁用它)。在本章结束,当你已完成了以上的内容时,可以尝试更改这些参数,看看它们如何影响算法的表现。

2.4.3 初始化

下一步就是初始化一个潜在解构成的种群。这通常是随机的,但偶尔也可能采用系统化的方法会更好,可以利用对搜索空间的已知信息来初始化种群。在这个例子中,种群中每个个体将随机初始化。我们可以为染色体的每个基因随机选择1或0,实现这一点。

初始化种群之前,我们需要创建两个类,一个管理并创建种群,另一个管理和创建种群的个体。这此类包含一些方法,例如获取个体适应度,或在种群中取得最适应的个体。

首先,让我们从创建Individual类开始。请注意,为了节省篇幅,我们省略了所有注释和方法的文档注释块!你可以在附带的Eclipse项目中找到该类的完全注释版本。

package chapter2;

public class Individual {
       private int[] chromosome;
       private double fitness = -1;

       public Individual(int[] chromosome) {
              // Create individual chromosome
              this.chromosome = chromosome;
       }

       public Individual(int chromosomeLength) {
              this.chromosome = new int[chromosomeLength];
              for (int gene = 0; gene < chromosomeLength; gene++) {
                     if (0.5 < Math.random()) {
                            this.setGene(gene, 1);
                     } else {
                            this.setGene(gene, 0);
                     }
              }
       }
       public int[] getChromosome() {
              return this.chromosome;
       }
       public int getChromosomeLength() {
              return this.chromosome.length;
       }
       public void setGene(int offset, int gene) {
              this.chromosome[offset] = gene;
       }
       public int getGene(int offset) {
              return this.chromosome[offset];
       }
       public void setFitness(double fitness) {
              this.fitness = fitness;
       }
       public double getFitness() {
              return this.fitness;
       }
       public String toString() {
              String output = "";
              for (int gene = 0; gene < this.chromosome.length; gene++) {
                     output += this.chromosome[gene];
              }
              return output;
       }
}

Individual类代表一个候选解,主要负责存储和操作一条染色体。请注意,Individual类也有两个构造方法。一个构造方法接受一个整数(代表染色体的长度),在初始化对象时将创建一条随机的染色体。另一个构造方法接受一个整数数组,用它作为染色体。

除了管理Individual的染色体,它也追踪个体的适应度值,也知道如何将自己打印为一个字符串。

下一步骤是创建Population类,它提供管理群体中一组个体所需的功能。

像往常一样,注释和文档注释块在这一章中已经省略了,一定要看看Eclipse项目,了解更多背景!

package chapter2;

import java.util.Arrays;
import java.util.Comparator;

public class Population {
       private Individual population[];
       private double populationFitness = -1;

       public Population(int populationSize) {
              this.population = new Individual[populationSize];
       }

       public Population(int populationSize, int chromosomeLength) {
              this.population = new Individual[populationSize];

              for (int individualCount = 0; individualCount <
              populationSize; individualCount++) {
                     Individual individual = new
                     Individual(chromosomeLength);
                     this.population[individualCount] = individual;
              }
       }

       public Individual[] getIndividuals() {
              return this.population;
       }

       public Individual getFittest(int offset) {
              Arrays.sort(this.population, new Comparator<Individual>() {
                     @Override
                     public int compare(Individual o1, Individual o2) {
                            if (o1.getFitness() > o2.getFitness()) {
                                   return -1;
                            } else if (o1.getFitness() < o2.getFitness()) {
                                   return 1;
                            }
                            return 0;
                     }
              });
              return this.population[offset];
       }

       public void setPopulationFitness(double fitness) {
              this.populationFitness = fitness;
       }

       public double getPopulationFitness() {
              return this.populationFitness;
       }

       public int size() {
              return this.population.length;
       }

       public Individual setIndividual(int offset, Individual individual) {
              return population[offset] = individual;
       }

              public Individual getIndividual(int offset) {
              return population[offset];
       }

       public void shuffle() {
              Random rnd = new Random();
              for (int i = population.length - 1; i > 0; i--) {
                           int index = rnd.nextInt(i + 1);
                           Individual a = population[index];
                           population[index] = population[i];
                           population[i] = a;
              }
       }
}

Population类相当简单,它的主要功能是保存由个体构成的一个数组,能够通过类方法方便地访问。例如getFittest()和setIndividual()这样的方法,能够访问并更新种群中的个体。除了保存个体,它也存储了种群的总体适应度,在稍后实现选择方法时,这很重要。

既然我们有了种群和个体类,就可以在GeneticAlgorithm类中将它们实例化。要做到这一点,只需创建一个名为“initPopulation”方法,放在GeneticAlgorithm类中的任意位置。

public class GeneticAlgorithm {
       /**
        * The constructor we created earlier is up here...
        */

       public Population initPopulation(int chromosomeLength) {
              Population population = new Population(this.populationSize,
              chromosomeLength);
              return population;
       }
       /**
        * We still have lots of methods to implement down here...
        */
}

既然有了Population和Individual类,我们就可以回到AllOnesGA类,开始使用initPopulation方法。回想一下,AllOnesGA类只有一个main方法,而且它代表本章前面介绍的伪代码。

在main方法中初始化种群时,还需要指定个体染色体的长度,这里,我们取长度为50:

public class AllOnesGA {
   public static void main(String[] args){
        // Create GA object
        GeneticAlgorithm ga = new GeneticAlgorithm(100, 0.01, 0.95, 0);

        // Initialize population
        Population population = ga.initPopulation(50);
   }
}
2.4.4 评估

在评估阶段,种群中的每个个体都计算其适应度值,并存储以便将来使用。为了计算个体的适应度,我们使用所谓的“适应度函数”。

遗传算法通过选择来引导进化过程,得到更好的个体。因为正是适应度函数使得这种选择成为可能,所以适应度函数应该设计良好,提供个体适应度的准确值,这很重要。如果适应度函数设计得不够好,可能需要更长的时间才能找到满足的最低标准的解,也可能根本找不到可以接受的解。

适应度函数往往是遗传算法中需要最多计算的组件。正因为如此,适应度函数做好优化,有助于预防瓶颈,让算法高效地运行。这很重要。

每个特定的优化问题,都需要一个特别开发的适应度函数。在“全一”问题的例子中,适应度函数相当简单,只需计算个体染色体中1的数量。

现在为GeneticAlgorithm类添加一个calcFitness方法。该方法将计算染色体中1的个数,然后除以染色体的长度,使输出规格化,在0和1之间。你可以在GeneticAlgorithm类的任意位置添加此方法,因此下面省略了其周围的代码:

public double calcFitness(Individual individual) {

       // Track number of correct genes
       int correctGenes = 0;

       // Loop over individual's genes
       for (int geneIndex = 0; geneIndex < individual.getChromosomeLength();
       geneIndex++) {
              // Add one fitness point for each "1" found
              if (individual.getGene(geneIndex) == 1) {
                     correctGenes += 1;
              }
       }

       // Calculate fitness
       double fitness = (double) correctGenes / individual.
       getChromosomeLength();

       // Store fitness
       individual.setFitness(fitness);

       return fitness;
}

我们也需要一个简单的辅助方法,遍历每个个体并评估它们(即对每个个体调用calcFitness)。我们称这个方法为evalPopulation,并将它也添加到GeneticAlgorithm类中。它看起来应该像下面这样,同样可以将它添加在任何位置:

public void evalPopulation(Population population) {
       double populationFitness = 0;

       for (Individual individual : population.getIndividuals()) {
              populationFitness += calcFitness(individual);
       }

       population.setPopulationFitness(populationFitness);
}

此时,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) { }
}

如果缺少其中任何属性或方法,请现在就回去实现它们。我们还有4个方法要在GeneticAlgorithm类中实现:isTerminationConditionMet、selectParent、crossoverPopulation和mutatePopulation。

2.4.5 终止检查

接下来需要检查终止条件是否已经满足。有许多不同类型的终止条件。有时可能知道最佳解是什么(更确切地说,是可能知道最佳解的适应度值),在这种情况下,我们可以直接检查正确解。然而,并非总是能够知道最佳解的适应度是什么,所以我们可以在解变得“足够好”时终止,即解超过某个适应度阈值。如果算法运行了太长时间(太多世代),我们也可以终止,或者可以结合一些因素,决定终止该算法。

由于“全一”问题很简单,事实上,我们知道正确的适应度应该是1,在这种情况下,找到正确的解就终止,这是合理的。但情况并非总是这样!事实上,很少会这样。但我们很幸运,这是一个简单的问题。

首先,我们必须构建一个函数,检查终止条件是否已发生。我们可以在GeneticAlgorithm类中添加如下代码来实现这一点。代码添加在任意位置,和往常一样,为了简洁,我们省略了其他的类。

public boolean isTerminationConditionMet(Population population) {
       for (Individual individual : population.getIndividuals()) {
              if (individual.getFitness() == 1) {
                     return true;
              }
       }

       return false;
}

上述方法检查种群中的每个个体,如果种群中任何个体的适应度为1,就返回true(这表明,我们已经找到了一个终止条件,可以停止)。

既然已经建立了终止条件,就可以在AllOnesGA类的主引导方法中添加一个循环,并使用新添加的终止检查作为其循环条件。如果终止检查返回true,遗传算法将停止循环,并返回其结果。

为了创建进化循环,将执行类AllOnesGA的main方法修改为如下所示。下面代码片段的前两行已经在main方法中。通过添加这段代码,我们将继续实现本章开头展示的伪代码:回忆一下,main方法是遗传算法伪代码的具体表现。下面是main方法现在的样子:

public static void main(String[] args) {
       // These two lines were already here:
       GeneticAlgorithm ga = new GeneticAlgorithm(100, 0.001, 0.95, 0);
        Population population = ga.initPopulation(50);

        // The following is the new code you should be adding:
        ga.evalPopulation(population);
        int generation = 1;

        while (ga.isTerminationConditionMet(population) == false) {
               // Print fittest individual from population
               System.out.println("Best solution: " + population.
               getFittest(0).toString());

               // Apply crossover
               // TODO!

               // 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());
}

我们添加了一个进化循环,检查isTerminationConditionMet的输出。main方法的新内容还包括在循环之前和循环之内添加了evalPopulation调用,用于追踪世代数目的generation变量,以及调试消息,以便让你知道每一代的最佳解是怎样的。

我们还增加了程序结束时的代码:循环退出时,打印关于最终解的一些信息。

然而,现在我们的遗传算法会运行,但不会永远进化!我们将陷入一个无限循环中,除非我们很幸运,随机产生的一个个体恰好是“全一”。你可以点击Eclipse中的“Run”按钮,直接看到这样的行为。相同的解将一遍又一遍地提交,循环永远不会结束。你不得不点击Eclipse控制台上方的“Terminate”按钮,强制程序停止。

为了继续建立我们的遗传算法,需要实现另外两个概念:交叉和变异。通过随机变异和最适者生存,这些概念实际上推动了种群向前进化。

2.4.6 交叉

现在,是时候开始运用变异和交叉来进化种群了。交叉算子是一个过程,在这个过程中,种群中的个体交换它们的遗传信息,希望创建一个新的个体,包含亲代基因组中最好的部分。

在交叉过程中,考虑种群中每个个体是否参与交叉,这时使用交叉率参数。通过比较交叉率和一个随机数,我们可以决定个体是应该参与交叉,还是应该直加入下一个种群,不受交叉影响。如果选择了一个个体参与交叉,就需要找到第二个亲代。要找到第二个亲代,我们需要在多种可能的选择方法中挑一种。