《Java遗传算法编程》—— 2.4 基本实现

2.4 基本实现

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

二进制遗传算法比较容易实现,对于解决许多种优化问题,它可能是非常有效的工具。你可能还记得第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 交叉

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

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

时间: 2024-10-06 04:24:38

《Java遗传算法编程》—— 2.4 基本实现的相关文章

《Java遗传算法编程》—— 第2章 实现一个基本遗传算法 2.1 实现之前

第2章 实现一个基本遗传算法 Java遗传算法编程 在本章中,我们将开始探索实现基本遗传算法的技术.本章开发的程序,将在后面的章节中进行修改,加入功能.同时,我们也将探讨基于遗传算法的参数和配置,以及它的性能会如何变化. 要尝试运行本节中的代码,需要先在计算机上安装Java JDK.你可以从Oracle的网站上免费下载并安装Java JDK: oracle.com/technetwork/java/javase/downloads/index.html 除了安装Java JDK,你也可以选择安装

《Java遗传算法编程》—— 第1章 简介 1.1 什么是人工智能

第1章 简介 Java遗传算法编程 数字计算机和信息时代的崛起,已经彻底改变了现代的生活方式.数字计算机的发明,使我们能够让生活的许多领域数字化.这种数字化让我们将许多繁琐的日常任务外包给计算机,而这些任务以前可能需要人来完成.这方面的一个日常例子是字处理应用程序,它们内置拼写检查功能,可以自动检查文档的拼写和语法错误. 随着计算机变得越来越快,计算能力越来越强,我们已经能够用它们来执行越来越复杂的任务,如理解人类语言,甚至比较准确地预测天气.这种不断的创新,让我们能够将越来越多的任务外包给计算

《Java遗传算法编程》—— 1.3 进化计算的历史

1.3 进化计算的历史 20世纪50年代,进化计算首次作为一种优化工具被尝试,当时的计算机科学家将达尔文的生物进化论思想应用于候选解构成的种群.他们建立理论,认为有可能应用进化算子,如交叉(它是生物繁殖的模拟)和变异(这是新的遗传信息添加到基因组中的过程).这些算子和选择压力共同作用,让遗传算法在一段时间后,能够"进化"出新的解. 在20世纪60年代,"进化策略"(应用自然选择和进化思想的一种优化技术)最初由Rechenberg(1965,1973)提出,他的想法后

《Java遗传算法编程》—— 2.2 基本遗传算法的伪代码

2.2 基本遗传算法的伪代码 基本遗传算法的伪代码如下: 1: generation = 0; 2: population[generation] = initializePopulation(populationSize); 3: evaluatePopulation(population[generation]); 3: While isTerminationConditionMet() == false do 4: parents = selectParents(population[ge

《Java遗传算法编程》—— 导读

目录 第1章 简介1.1 什么是人工智能1.2 生物学类比1.3 进化计算的历史1.4 进化计算的优势1.5 生物进化1.6 基本术语1.7 搜索空间1.8 参数1.9 基因表示1.10 终止1.11 搜索过程1.12 参考文献 第2章 实现一个基本遗传算法2.1 实现之前2.2 基本遗传算法的伪代码2.3 关于本书的代码示例2.4 基本实现2.5 轮盘赌选择2.6 交叉方法2.7 交叉伪代码2.8 交叉实现2.9 小结2.10 练习 第3章 机器人控制器 第4章 旅行商 第5章 排课 第6章

《Java遗传算法编程》—— 1.9 基因表示

1.9 基因表示 除了这些参数,影响遗传算法表现的另一个因素是所用的基因表示.这是遗传信息在染色体内的编码方式.更好的表示会让解的编码方式既表现力强,又容易进化.Holland(1975)的遗传算法是基于二进制基因表示.他建议染色体用含有0和1的序列.这种二进制表示可能是目前最简单的编码,但对于很多问题表现力不是很够,不是合适的首选.请考虑一个例子,用二进制表示来编码一个整数,它将被优化,用于某函数.在这个例子中,"000"代表0,而"111"代表7,就像通常二进制

《Java遗传算法编程》—— 2.10 练习

2.10 练习 1.运行遗传算法几次,观察进化过程的随机性.它通常需要多少代来找到这个问题的一个解? 2.扩大和减小种群规模.减小种群规模如何影响算法的速度?它是否也影响找到一个解需要的世代数?扩大种群规模如何影响算法的速度?它如何影响找到一个解需要的世代数? 3.将变异率设置为0.这将如何影响遗传算法寻找解的能力?使用高变异率,如何影响算法? 4.使用低交叉率.低交叉率下,算法表现如何? 5.尝试用较短及较长的染色体,减少和增加问题的复杂性.在处理更短或更长的染色体时,不同的参数是否工作得更好

《Java遗传算法编程》—— 2.9 小结

2.9 小结 在本章中,你已经学会了实现遗传算法的基本知识.本章开头的伪代码提供了一个通用的概念模型,针对本书其余部分所有要实现的遗传算法:每个遗传算法将初始化并评估种群,然后进入一个循环,进行交叉.变异和再评估.仅当终止条件满足时,才退出循环. 在本章中,你建立了遗传算法的支持组件,尤其是Individual和Population类,在后面的章节中基本上会复用它们.然后你专门建立了GeneticAlgorithm类,具体解决"全一"问题,并成功地运行了它. 你还了解了以下内容:虽然每

《Java遗传算法编程》—— 1.6 基本术语

1.6 基本术语 遗传算法建立在生物进化的概念上,因此,如果你熟悉进化的术语,可能在学习遗传算法时会发现术语有所重叠.这种领域间的相似性是当然的,因为进化算法,确切来说是遗传算法,类似于自然界中发现的过程. 术语 在更深入遗传算法领域之前,我们先了解一些基本的语言和术语,这很重要.随着本书的推进,我们会根据需要引入更复杂的术语.下面是一些较常见的术语的列表,可供参考. 种群:这就是一个候选解集合,可以有变异和交叉这样的遗传操作应用于它们. 候选解:给定问题的一个可能的解. 基因:组成染色体的不可