1. 概述

本系列旨在解释遗传算法的概念。遗传算法的设计灵感来源于自然界的进化过程,通过选择、重组和突变等机制来寻找问题的解决方案。

我们首先从最简单的二进制遗传算法示例入手,逐步讲解其工作原理。

2. 遗传算法的工作原理

遗传算法是进化计算的一部分,这是一种快速发展的人工智能领域。

算法从一组称为种群(由个体表示)开始。通过比较新旧种群的性能,旧种群的部分解会被用于形成新的种群,期望新种群的性能优于旧种群。

个体被选中以形成新解(后代)的过程依据它们的适应度进行——越适合的个体,繁殖的机会越大。

3. 二进制遗传算法

让我们来看看简单遗传算法的基本流程。

3.1. 初始化

在初始化步骤中,我们随机生成一个作为初始解决方案的种群。首先,我们需要确定种群的大小以及我们期望得到的最终解:

SimpleGeneticAlgorithm.runAlgorithm(50,
  "1011000100000100010000100000100111001000000100000100000000001111");

在这个例子中,种群大小为50,正确解由我们可以随时改变的二进制位串表示。

接下来,我们将保存期望的解,并创建一个随机种群:

setSolution(solution);
Population myPop = new Population(populationSize, true);

现在我们准备好运行程序的主要循环。

3.2. 适应度评估

在程序的主要循环中,我们将根据适应度函数评估每个个体(换句话说,个体越优秀,适应度函数值越高):

while (myPop.getFittest().getFitness() < getMaxFitness()) {
    System.out.println(
      "Generation: " + generationCount
      + " Correct genes found: " + myPop.getFittest().getFitness());
    
    myPop = evolvePopulation(myPop);
    generationCount++;
}

首先,我们来解释如何找到最适个体

public int getFitness(Individual individual) {
    int fitness = 0;
    for (int i = 0; i < individual.getDefaultGeneLength()
      && i < solution.length; i++) {
        if (individual.getSingleGene(i) == solution[i]) {
            fitness++;
        }
    }
    return fitness;
}

如图所示,我们逐位比较两个个体。如果没有找到完美解,就需要进行下一步,即种群的进化。

3.3. 后代

在这个阶段,我们需要创建一个新的种群。首先,需要根据适应度从种群中选择两个父代个体。值得注意的是,让当前世代的最佳个体无变异地传递到下一代是有益的,这被称为精英策略

if (elitism) {
    newPopulation.getIndividuals().add(0, pop.getFittest());
    elitismOffset = 1;
} else {
    elitismOffset = 0;
}

为了选择两个最佳个体,我们将应用锦标赛选择策略

private Individual tournamentSelection(Population pop) {
    Population tournament = new Population(tournamentSize, false);
    for (int i = 0; i < tournamentSize; i++) {
        int randomId = (int) (Math.random() * pop.getIndividuals().size());
        tournament.getIndividuals().add(i, pop.getIndividual(randomId));
    }
    Individual fittest = tournament.getFittest();
    return fittest;
}

每场锦标赛的获胜者(适应度最高的个体)将进入下一阶段,即交叉

private Individual crossover(Individual indiv1, Individual indiv2) {
    Individual newSol = new Individual();
    for (int i = 0; i < newSol.getDefaultGeneLength(); i++) {
        if (Math.random() <= uniformRate) {
            newSol.setSingleGene(i, indiv1.getSingleGene(i));
        } else {
            newSol.setSingleGene(i, indiv2.getSingleGene(i));
        }
    }
    return newSol;
}

在交叉过程中,我们在随机选择的位置上交换所选个体的位。整个过程在以下循环中执行:

for (int i = elitismOffset; i < pop.getIndividuals().size(); i++) {
    Individual indiv1 = tournamentSelection(pop);
    Individual indiv2 = tournamentSelection(pop);
    Individual newIndiv = crossover(indiv1, indiv2);
    newPopulation.getIndividuals().add(i, newIndiv);
}

可见,交叉后,我们将新的后代放入新的种群中,这一步称为接受

最后,可以执行突变。突变用于保持种群从一代到下一代的遗传多样性。我们使用了位翻转类型的突变,其中随机位被简单地反转:

private void mutate(Individual indiv) {
    for (int i = 0; i < indiv.getDefaultGeneLength(); i++) {
        if (Math.random() <= mutationRate) {
            byte gene = (byte) Math.round(Math.random());
            indiv.setSingleGene(i, gene);
        }
    }
}

关于所有类型的交叉和突变的详细描述,请参考这篇教程

我们重复3.2和3.3小节的步骤,直到达到终止条件,例如找到最佳解。

4. 小贴士与技巧

为了实现高效的遗传算法,我们需要调整一组参数。这部分将给出一些基本的建议,帮助你从最重要的参数开始:

  • 交叉率 - 应该很高,约为80%-95%
  • 突变率 - 应该非常低,大约在**0.5%-1%**。
  • 种群大小 - 良好的种群大小约为20-30,但对某些问题,50-100可能更好
  • 选择 - 可以使用基本的轮盘赌选择,并结合精英主义概念
  • 交叉和突变类型 - 取决于编码和问题

请注意,这些参数调整的推荐通常是基于遗传算法经验研究的结果,可能会因所解决的问题而有所不同。

5. 总结

本教程介绍了遗传算法的基础知识。无需事先了解该领域,只需要基本的计算机编程技能,你就可以学习遗传算法。

本教程中代码片段的完整源代码可以在GitHub项目中找到。

另外,我们使用了Lombok生成getter和setter。有关如何在IDE中正确配置它的文章,可以参考这篇文章

欲了解更多遗传算法实例,请查看我们系列的其他文章: