经验教训:遗传算法入门






4.97/5 (15投票s)
遗传算法入门,简要提及生物学,并举例说明如何找到复杂数学方程的一个解。
C# 和 C++ 示例均使用 MS Visual Studio Community 2015 创建。Java 示例使用 Eclipse neon.3 创建。源代码兼容不同平台。
引言
本文旨在介绍遗传算法,该算法广泛应用于机器学习和人工智能等现代技术中。本文面向初学者,作为主课程的补充阅读材料,旨在激励他们学习更高级的主题。对于所有希望提高知识和技能的人来说,本文也可能是有益的。
作者力求使本文及源代码尽可能简单,并希望读者在阅读文本和运行代码时能有所收获。作者非常乐意收到读者的任何反馈,以改进本文。
目录
背景
动机
作为巴库国立大学的讲师,作者观察到学生们的编程技能和天赋水平各不相同。年轻专业人才的培养不仅需要知识的传递,还需要学习和自我发展的动力。提供补充阅读材料以拓宽学生的视野、加深对信息技术的理解变得尤为重要。这些补充阅读材料并非强制性,但强烈推荐。有才华和技能的学生可以通过实践获得经验,而其他人则可以丰富他们的知识。
如何阅读本文
本文包含两个主要部分:“生物学背景”和“算法实现”。“生物学背景”章节以获取具有所需品质的狗的个体为例。这个例子非常简单。真实的生物过程要复杂得多。但这个例子的主要目的是解释遗传算法中使用的概念。对于经验丰富的专业人士来说,这一章可能显得不必要且令人厌烦,但对于新手来说,这个介绍可能至关重要。
“算法实现”章节解释了如何编程和运行遗传算法。本章使用三种编程语言描述了算法的实现:C#、Java 和 C++。源代码可供下载(参见文章顶部的链接)。
生物学背景
遗传算法的理念来源于自然。所有生物都能通过世代的繁衍而改变,从而适应环境的变化。人们一直利用这一特性来培育具有期望品质的新品种动植物。我们来考虑一个简单的例子。这个例子将揭示过程如何进行以及一些术语的含义。
假设有一群灰色的狗,需要得到白色的狗。下图显示了现有的狗群。
如果在观察狗群时,读者会发现一些个体并非完全是灰色的,而是带有小的白色斑点。这是因为它们在繁殖时发生了突变(这是自然规律)。“突变”和“繁殖”这两个术语在遗传算法中也同样使用。因此,这些概念将在后续的描述中使用。术语“种群”也将被用来代替“狗群”,原因相同。
下一步,我们从种群中选择带有白色斑点的个体。“选择”也是遗传算法(以及生物学和农业)中使用的术语。选择的个体如下图所示。
如果选择的个体进行繁殖,就会产生新的种群。虽然新个体(后代)的品质是其父母品质的组合,但在繁殖过程中(这是自然规律。科学家利用辐射来增强突变),品质会发生偶然变化,称为“突变”。由于突变是随机的,最终的种群将包含三种类型的个体:
- 带有少量或没有白色斑点的个体
- 具有相同大小白色斑点的个体(大多数情况下。我们可以说这些个体没有发生突变。)
- 具有更大白色斑点的个体
下图显示了新的种群。读者可以将其与第一个种群进行比较,以查看差异。
下一步是选择具有更大白色斑点的个体。下图显示了选定的个体。读者也可以将此选择与之前的选择进行比较。
然后繁殖它们以获得下一个种群,如下所示。
选择“更好”的个体,繁殖它们形成新种群,然后一遍又一遍地重复这个过程,直到出现“更好”的个体,直到出现完全白色的(“最佳”)个体。下图展示了过程。
第三次选择。
第三次选择后形成的种群。
第四次选择。
第四次选择后形成的种群。
第五次选择。
第五次选择后形成的种群。
对于这个种群,最终出现了一些完全白色的个体。问题解决了。但如果继续这个过程,“白色”种群就会出现。但由于突变,一些物种会带有灰色的斑点。尽管继续这个过程在生物学和农业中可能很有用,但许多实际的遗传算法会在找到解决方案后停止。只有少量特定情况需要遗传算法继续进行(例如,模拟连续过程的 AI 解决方案)。
“白色”选择。
“白色”种群。
章节总结
本章的总结如下:
- 术语“个体”(specimen)表示一个实例/项。
- 术语“种群”(population)表示一群个体,通常是同一代。
- 术语“选择”(selection)表示一组根据期望标准从种群中选出的个体,用于未来的繁殖。
- 术语“繁殖”(reproduction)表示从上一次选择中获得新的种群(新一代),以增加具有通过组合其父母创建的新个体而产生的个体数量。
- 术语“基因”(genes)表示个体的内部结构,负责形成其所有者的品质。
- 术语“突变”(mutation)表示个体品质的偶然变化。换句话说,突变是基因的偶然变化。
- 持续的选择、带偶然突变的繁殖,可以获得具有期望品质的个体。
算法实现
在实现遗传算法之前,清晰的任务陈述至关重要。问题完全明确后,必须构建模型。模型必须反映适用于计算的生物过程。模型需要阐明以下内容:
- 什么是“个体”?
- 什么是“基因”?
- 个体如何繁殖以形成种群?
- 如何实现突变?
- 种群的大小是多少?
- 选择的大小是多少?
- 如何量化评估个体与解决方案的亲和度?
- 何时可以认为个体是解决方案?
对于简单的问题,模型可以直接编写到计算机程序中。复杂的问题可能需要进行数学建模、分析,并使用专用软件工具进行设计。本例很简单,读者可以在文本和源文件中找到模型的元素。
任务陈述
让我们考虑以下方程:
19.39281272 * X5 + 7.82018991 * X4 + 35.12849546 * X3 - 28.09127103 * X2 + 3.30129489 * X = 20351.07006276
这是一个五次方程,很难用简单的数学方法求解。这是一个很好的起点来学习遗传算法,因为它易于理解、建模和编程。
实现
遗传算法使用面向对象编程来实现。尽管使用了三种编程语言,但主要的类、属性和方法却相当相似。下面对源代码的描述将有助于详细理解模型(源代码可通过文章顶部的链接下载)。
Specimen 类
Class Specimen 代表方程的一个实例。所要求的方程可以表示为如下形式:
F
= 19.39281272 * X
5 + 7.82018991 * X
4 + 35.12849546 * X
3 - 28.09127103 * X
2 + 3.30129489 * X
- 20351.07006276
在这种情况下,方程被指定为“个体”,其中F
与零的距离表示X
与解的亲和度。由于X
是变量,因此选择X
作为基因。
基因的初始值设置为 10(参见源代码),因为 10 的 5 次方是 100000,这在范围上接近 20351.07006276。这个基因值并不重要——它只影响迭代次数和计算时间(即使从黑狗群也能得到白狗)。对于当前示例,它可以设置为任何值,而不是 0
(当前的突变函数很简单,无法突变零基因)。读者可以尝试将基因设置为不同的值,运行示例,并观察算法的工作原理。
Class Specimen
包含方法 CalculateAffinity()
,该方法实现当前基因值的方程计算。CalculateAffinity()
将计算值设置到成员 Affinity
中,该成员用于在 Class SpecimenWorkshop
中对种群进行排序,以便后续选择。在排序之前计算 Affinity
显著减少了计算时间。
C++ 版本的 Class Specimen
包含方法 Clone()
,该方法使内存管理代码更易读。C# 和 Java 具有垃圾回收器,因此在这种简单情况下无需担心内存管理(一些内存管理技术可以提高多线程高负载解决方案的性能,即使对于 C# 或 Java 也是如此)。
using System;
namespace L2L_I2GA
{
// Class that represents the Specimen
public class Specimen
{
// The genes
public double[] Genes;
// Affinity to the solution
public double Affinity;
// Constructor that creates the Specimen
// instance with initial genes
public Specimen()
{
// Assume one double value as the genes
Genes = new double[1];
// Set up initial value to the genes
Genes[0] = 10;
}
// Calculates affinity of this instance to the solution
// Contains the equation formula
public void CalculateAffinity()
{
double s1 = Math.Pow(Genes[0], 5) * 19.39281272;
double s2 = Math.Pow(Genes[0], 4) * 7.82018991;
double s3 = Math.Pow(Genes[0], 3) * 35.12849546;
double s4 = Math.Pow(Genes[0], 2) * 28.09127103;
double s5 = Genes[0] * 3.30129489;
double f = s1 + s2 + s3 - s4 + s5;
// Need positive value to evaluate proximity
// 0 is the best value
Affinity = Math.Abs(f - 20351.07006276);
}
}
}
// Class that represents the Specimen
public class Specimen
{
// The genes
public double[] Genes;
// Affinity to the solution
public double Affinity;
// Constructor that creates the Specimen
// instance with initial genes
public Specimen()
{
// Assume one double value as the genes
Genes = new double[1];
// Set up initial value to the genes
Genes[0] = 10;
}
// Calculates affinity of this instance to the solution
// Contains the equation formula
public void CalculateAffinity()
{
double s1 = Math.pow(Genes[0], 5) * 19.39281272;
double s2 = Math.pow(Genes[0], 4) * 7.82018991;
double s3 = Math.pow(Genes[0], 3) * 35.12849546;
double s4 = Math.pow(Genes[0], 2) * 28.09127103;
double s5 = Genes[0] * 3.30129489;
double f = s1 + s2 + s3 - s4 + s5;
// Need positive value to evaluate proximity
// 0 is the best value
Affinity = Math.abs(f - 20351.07006276);
}
}
#ifndef H_SPECIMEN
#define H_SPECIMEN
// Class that represents the Specimen
class Specimen
{
public:
// The genes
double* Genes;
// Affinity to the solution
double Affinity;
// Constructor that creates the Specimen
// instance with initial genes
Specimen();
// Destructor. Frees memory for the Genes
~Specimen();
// Clone the Specimen for simple memory management
Specimen* Clone();
// Calculates affinity of this instance to the solution
// Contains the equation formula
void CalculateAffinity();
};
#include "Specimen.h"
#include <math.h>
#include <cmath>
// Constructor that creates the Specimen
// instance with initial genes
Specimen::Specimen()
{
// Assume one double value as the genes
Genes = new double[1];
// Set up initial value to the genes
Genes[0] = 10;
}
// Destructor. Frees memory for the Genes
Specimen::~Specimen()
{
delete[] Genes;
}
// Clone the Specimen for simple memory management
Specimen* Specimen::Clone()
{
Specimen* s = new Specimen();
s->Genes[0] = Genes[0];
s->Affinity = Affinity;
return s;
}
// Calculates affinity of this instance to the solution
// Contains the equation formula
void Specimen::CalculateAffinity()
{
double s1 = pow(Genes[0], 5) * 19.39281272;
double s2 = pow(Genes[0], 4) * 7.82018991;
double s3 = pow(Genes[0], 3) * 35.12849546;
double s4 = pow(Genes[0], 2) * 28.09127103;
double s5 = Genes[0] * 3.30129489;
double f = s1 + s2 + s3 - s4 + s5;
// Need positive value to evaluate proximity
// 0 is the best value
Affinity = std::abs(f - 20351.07006276);
}
SpecimenWorkshop 类
Class SpecimenWorkshop
设计用于处理个体、它们的种群和选择。个体的繁殖和突变是随机过程。因此,C# 和 Java 版本的类都包含随机数生成器。C++ 版本则使用 rand()
函数。PopulationSize
和 SelectionSize
分别表示种群和选择的大小。MaxMutationPercent
是基因变化的程度。它可以设置为任何值。MutationLikelyhoodPercent
分配了突变的可能性,允许只有一部分个体发生突变,而另一部分保持不变。Epsilon
用于设置亲和度与目标值(表示找到解的值)比较时的精度。遗传算法包含大量的计算。提高精度(可以通过减小 Epsilon 来设置)会显著增加计算时间。读者可以更改该值并观察示例的工作原理。
该类实现两个 GeneratePopulation
方法。一个方法构建初始种群。对初始种群中所有个体的突变会产生差异最大的个体,从而增加了获得接近解的个体的几率。否则,种群将包含相似的个体,这对未来的计算毫无用处(从所有个体都有白色斑点的狗群中选择比从大多数个体没有白色斑点的狗群中选择要好)。
第二个 GeneratePopulation
方法基于从选择中随机选择的配对繁殖来产生新的种群。本例中使用的繁殖算法很简单但效果很好。它随机选择两个亲本并为它们创建一个后代。其他繁殖策略可以改进算法。有许多不同的策略可以使用。例如,第一个(最佳)个体与其他个体配对繁殖,以及后代的重复繁殖。将选择中的个体保留在新种群中或将其移除也是两种不同的策略。有经验的读者可以测试不同的策略。
方法 ReproduceNew
为两个亲本创建一个新的“后代”个体。它将后代的基因设置为两个亲本基因的平均值。创建后,基因会根据 MutationLikelyhoodPercent
和 MaxMutationPercent
的值发生突变(或不发生)。
方法 Select
通过执行两个主要操作来实现。首先,该方法以使数组包含最佳个体在前、最差个体在后的方式对种群进行排序。其次,最佳个体被复制到选择中以供将来计算。
using System;
namespace L2L_I2GA
{
// Class that performs main actions with the specimens and the population
public static class SpecimenWorkshop
{
// Random generator
static Random rnd = new Random();
// Number of specimens in the population
public static int PopulationSize;
// Number of specimens in the selection
public static int SelectionSize;
// Power of mutations
// 0 - no mutations
// 100 - mutations up to whole gene value
// 200 - mutations up to twice gene value
public static double MaxMutationPercent;
// Likelihood of mutation
// 0 - no mutations, 100 - mutations for all cases
public static int MutationLikelyhoodPercent;
// Maximal affinity that is considered
// for the solution found
public static double Epsilon;
// Generate initial population
public static Specimen[] GeneratePopulation()
{
// Creates array representing the population
Specimen[] p = new Specimen[PopulationSize];
// Creates specimens
// Mutation of all specimens in initial population
// increases variance that increases chance to
// get better instance.
for (int i = 0; i < PopulationSize; i++)
{
p[i] = new Specimen();
Mutate(p[i]);
// Calculate Affinity for new specimens
p[i].CalculateAffinity();
}
return p;
}
// Generate population by reproduction of selection
public static Specimen[] GeneratePopulation(Specimen[] selection)
{
// Creates array representing the population
Specimen[] p = new Specimen[PopulationSize];
// Copy instances from the selection to keep them
// in new generation of population
for (int i = 0; i < SelectionSize; i++)
{
p[i] = selection[i];
}
// Creates new specimens by reproducing two parents
// Parents are selected randomly from the selection.
int chield_index = SelectionSize;
int parent1_index;
int parent2_index;
while(chield_index < PopulationSize)
{
// Select two parents randomly in way
// they are different instances
do
{
parent1_index = rnd.Next(selection.Length);
parent2_index = rnd.Next(selection.Length);
} while (parent1_index == parent2_index);
// Creates new specimen
p[chield_index] = ReproduceNew(
selection[parent1_index],
selection[parent2_index]);
chield_index++;
}
return p;
}
// Reproduce new specimen on base of two parents
public static Specimen ReproduceNew(Specimen a, Specimen b)
{
Specimen s= new Specimen();
// Inherit genes as the average oh the parents' genes
s.Genes[0] = (a.Genes[0] + b.Genes[0]) / 2;
// Mutate if likelihood allows
int ml = rnd.Next(101);
if (ml <= MutationLikelyhoodPercent)
{
Mutate(s);
}
// Calculate Affinity for new specimen
s.CalculateAffinity();
return s;
}
// Select the best specimens from the population
public static Specimen[] Select(Specimen[] population)
{
// Sort population by increasing the affinity
// The best specimens are moving to start of the array
Sort(population);
// Create set of selected specimens
Specimen[] selected = new Specimen[SelectionSize];
// Copy best specimens into the selection
for (int i = 0; i < SelectionSize; i++)
{
selected[i] = population[i];
}
return selected;
}
// Sort the population
public static void Sort(Specimen[] population)
{
// Use standard procedure
Array.Sort<specimen>(population, CompareSpecimens);
}
// Comparison function for the standard Sort procedure
public static int CompareSpecimens(Specimen a, Specimen b)
{
if(a.Affinity < b.Affinity)
{
return -1;
} else
if (a.Affinity > b.Affinity)
{
return 1;
}
else
{
return 0;
}
}
// Mutate the specimen
public static void Mutate(Specimen sp)
{
// Calculate Mutation Factor that is random value between 0 and MaxMutationPercent
// calculates as ratio between 0 and 1
double MutationFactor = (MaxMutationPercent / 100.0) * (rnd.Next(1001) / 1000.0);
// Set mutation to negative with 50% likelihood
if (rnd.Next(10) < 5)
{
MutationFactor = -MutationFactor;
}
// Calculate new gene
sp.Genes[0] = sp.Genes[0] * (1 + MutationFactor);
}
}
}
import java.util.Random;
// Class that performs main actions with the specimens and the population
public class SpecimenWorkshop
{
// Random generator
static Random rnd = new Random();
// Number of specimens in the population
public static int PopulationSize;
// Number of specimens in the selection
public static int SelectionSize;
// Power of mutations
// 0 - no mutations
// 100 - mutations up to whole gene value
// 200 - mutations up to twice gene value
public static double MaxMutationPercent;
// Likelihood of mutation
// 0 - no mutations, 100 - mutations for all cases
public static int MutationLikelyhoodPercent;
// Maximal affinity that is considered
// for the solution found
public static double Epsilon;
// Generate initial population
public static Specimen[] GeneratePopulation()
{
// Creates array representing the population
Specimen[] p = new Specimen[PopulationSize];
// Creates specimens
// Mutation of all specimens in initial population
// increases variance that increases chance to
// get better instance.
for (int i = 0; i < PopulationSize; i++)
{
p[i] = new Specimen();
Mutate(p[i]);
// Calculate Affinity for new specimens
p[i].CalculateAffinity();
}
return p;
}
// Generate population by reproduction of selection
public static Specimen[] GeneratePopulation(Specimen[] selection)
{
// Creates array representing the population
Specimen[] p = new Specimen[PopulationSize];
// Copy instances from the selection to keep them
// in new generation of population
for (int i = 0; i < SelectionSize; i++)
{
p[i] = selection[i];
}
// Creates new specimens by reproducing two parents
// Parents are selected randomly from the selection.
int chield_index = SelectionSize;
int parent1_index;
int parent2_index;
while(chield_index < PopulationSize)
{
// Select two parents randomly in way
// they are different instances
do
{
parent1_index = rnd.nextInt(selection.length);
parent2_index = rnd.nextInt(selection.length);
} while (parent1_index == parent2_index);
// Creates new specimen
p[chield_index] = ReproduceNew(selection[parent1_index], selection[parent2_index]);
chield_index++;
}
return p;
}
// Reproduce new specimen on base of two parents
public static Specimen ReproduceNew(Specimen a, Specimen b)
{
Specimen s= new Specimen();
// Inherit genes as the average oh the parents' genes
s.Genes[0] = (a.Genes[0] + b.Genes[0]) / 2;
// Mutate if likelihood allows
int ml = rnd.nextInt(101);
if (ml <= MutationLikelyhoodPercent)
{
Mutate(s);
}
// Calculate Affinity for new specimen
s.CalculateAffinity();
return s;
}
// Select the best specimens from the population
public static Specimen[] Select(Specimen[] population)
{
// Sort population by increasing the affinity
// The best specimens are moving to start of the array
Sort(population);
// Create set of selected specimens
Specimen[] selected = new Specimen[SelectionSize];
// Copy best specimens into the selection
for (int i = 0; i < SelectionSize; i++)
{
selected[i] = population[i];
}
return selected;
}
// Sort the population
public static void Sort(Specimen[] population)
{
Specimen temp;
for (int i = 0; i < PopulationSize; i++)
{
for (int j = 0; j < PopulationSize; j++)
{
if (population[i].Affinity < population[j].Affinity)
{
temp = population[i];
population[i] = population[j];
population[j] = temp;
}
}
}
}
// Mutate the specimen
public static void Mutate(Specimen sp)
{
// Calculate Mutation Factor that is random value between 0 and MaxMutationPercent
// calculates as ratio between 0 and 1
double MutationFactor = (MaxMutationPercent / 100.0) * (rnd.nextInt(1001) / 1000.0);
// Set mutation to negative with 50% likelihood
if (rnd.nextInt(10) < 5)
{
MutationFactor = -MutationFactor;
}
// Calculate new gene
sp.Genes[0] = sp.Genes[0] * (1 + MutationFactor);
}
}
#ifndef H_SPECIMENWORKSHOP
#define H_SPECIMENWORKSHOP
#include "Specimen.h"
// Class that performs main actions with the specimens and the population
class SpecimenWorkshop
{
public:
// Number of specimens in the population
static int PopulationSize;
// Number of specimens in the selection
static int SelectionSize;
// Power of mutations
// 0 - no mutations
// 100 - mutations up to whole gene value
// 200 - mutations up to twice gene value
static double MaxMutationPercent;
// Likelihood of mutation
// 0 - no mutations, 100 - mutations for all cases
static int MutationLikelyhoodPercent;
// Maximal affinity that is considered
// for the solution found
static double Epsilon;
// Generate initial population
static Specimen** GeneratePopulation();
// Generate population by reproduction of selection
static Specimen** GeneratePopulation(Specimen** selection);
// Reproduce new specimen on base of two parents
static Specimen* ReproduceNew(Specimen* a, Specimen* b);
// Select the best specimens from the population
static Specimen** SpecimenWorkshop::Select(Specimen** population);
// Sort the population
static void SpecimenWorkshop::Sort(Specimen** population);
// Mutate the specimen
static void SpecimenWorkshop::Mutate(Specimen* sp);
};
#endif
#include "SpecimenWorkshop.h"
#include <stdlib.h>
// Number of specimens in the population
int SpecimenWorkshop::PopulationSize;
// Number of specimens in the selection
int SpecimenWorkshop::SelectionSize;
// Power of mutations
// 0 - no mutations
// 100 - mutations up to whole gene value
// 200 - mutations up to twice gene value
double SpecimenWorkshop::MaxMutationPercent;
// Likelihood of mutation
// 0 - no mutations, 100 - mutations for all cases
int SpecimenWorkshop::MutationLikelyhoodPercent;
// Maximal affinity that is considered
// for the solution found
double SpecimenWorkshop::Epsilon;
// Generate initial population
Specimen** SpecimenWorkshop::GeneratePopulation()
{
// Creates array representing the population
Specimen** p = new Specimen*[PopulationSize];
// Creates specimens
// Mutation of all specimens in initial population
// increases variance that increases chance to
// get better instance.
for (int i = 0; i < PopulationSize; i++)
{
p[i] = new Specimen();
Mutate(p[i]);
// Calculate Affinity for new specimens
p[i]->CalculateAffinity();
}
return p;
}
// Generate population by reproduction of selection
Specimen** SpecimenWorkshop::GeneratePopulation(Specimen** selection)
{
// Creates array representing the population
Specimen** p = new Specimen*[PopulationSize];
// Copy instances from the selection to keep them
// in new generation of population
for (int i = 0; i < SelectionSize; i++)
{
p[i] = selection[i]->Clone();
}
// Creates new specimens by reproducing two parents
// Parents are selected randomly from the selection.
int chield_index = SelectionSize;
int parent1_index;
int parent2_index;
while (chield_index < PopulationSize)
{
// Select two parents randomly in way
// they are different instances
do
{
parent1_index = rand() % SelectionSize;
parent2_index = rand() % SelectionSize;
} while (parent1_index == parent2_index);
// Creates new specimen
p[chield_index] = ReproduceNew(selection[parent1_index], selection[parent2_index]);
chield_index++;
}
return p;
}
// Reproduce new specimen on base of two parents
Specimen* SpecimenWorkshop::ReproduceNew(Specimen* a, Specimen* b)
{
Specimen* s = new Specimen();
// Inherit genes as the average oh the parents' genes
s->Genes[0] = (a->Genes[0] + b->Genes[0]) / 2;
// Mutate if likelihood allows
int ml = rand() % 101;
if (ml <= MutationLikelyhoodPercent)
{
Mutate(s);
}
// Calculate Affinity for new specimen
s->CalculateAffinity();
return s;
}
// Select the best specimens from the population
Specimen** SpecimenWorkshop::Select(Specimen** population)
{
// Sort population by increasing the affinity
// The best specimens are moving to start of the array
Sort(population);
// Create set of selected specimens
Specimen** selected = new Specimen*[SelectionSize];
// Copy best specimens into the selection
for (int i = 0; i < SelectionSize; i++)
{
selected[i] = population[i]->Clone();
}
return selected;
}
// Sort the population
void SpecimenWorkshop::Sort(Specimen** population)
{
Specimen* temp;
for (int i = 0; i < PopulationSize; i++)
{
for (int j = 0; j < PopulationSize; j++)
{
if (population[i]->Affinity < population[j]->Affinity)
{
temp = population[i];
population[i] = population[j];
population[j] = temp;
}
}
}
}
// Mutate the specimen
void SpecimenWorkshop::Mutate(Specimen* sp)
{
// Calculate Mutation Factor that is random value between 0 and MaxMutationPercent
// calculates as ratio between 0 and 1
double MutationFactor = (MaxMutationPercent / 100.0) * (rand() % 1001 / 1000.0);
// Set mutation to negative with 50% likelihood
if ((rand() % 10) < 5)
{
MutationFactor = -MutationFactor;
}
// Calculate new gene
sp->Genes[0] = sp->Genes[0] * (1 + MutationFactor);
}
</stdlib.h>
Solver 类
Class Solver
在最高抽象级别上代表遗传算法。方法 Initialize()
通过设置选项和生成初始种群来初始化算法。方法 Run()
运行计算循环直到找到解。计算循环包含两个主要步骤——从当前种群中选择最佳个体,并通过繁殖选定的个体来更新当前种群。如果选择中的最佳个体的亲和度足够接近零(对于本例),循环就会停止。该个体的基因包含方程的解。C++ 版本的类还包含内存管理代码(删除旧对象)。
using System;
namespace L2L_I2GA
{
// Class that implements the Genetic Algorithm
// at the highest abstraction level
public static class Solver
{
// Current Population
static Specimen[] Current_Population;
// Current Selection
static Specimen[] Current_Selection;
// Current Proximity
static double Current_Proximity;
// Current Iteration
static int Current_Iteration;
// Initialize the algorithm
public static void Initialize()
{
// Set up options for the algorithm
SpecimenWorkshop.PopulationSize = 10000;
SpecimenWorkshop.SelectionSize = 1000;
SpecimenWorkshop.MaxMutationPercent = 500;
SpecimenWorkshop.MutationLikelyhoodPercent = 90;
SpecimenWorkshop.Epsilon = 0.000000001;
// Generate initial population
Current_Population = SpecimenWorkshop.GeneratePopulation();
Current_Iteration = 0;
}
// Run the algorithm
public static void Run()
{
// Set Current_Proximity to the biggest value
Current_Proximity = double.MaxValue;
// Loop while Current_Proximity is not less than the Epsilon
while (SpecimenWorkshop.Epsilon <= Current_Proximity)
{
// Select the best specimens
Current_Selection = SpecimenWorkshop.Select(Current_Population);
// Calculate proximity for the top-selected (the best) specimen
Current_Proximity = Current_Selection[0].Affinity;
// Report proximity and found solution for the current iteration
Console.WriteLine(
"{0}\t{1}\t{2}",
Current_Iteration,
Current_Proximity,
Current_Selection[0].Genes[0]);
// End the calculations if Current_Proximity is less than the Epsilon
if (Current_Proximity < SpecimenWorkshop.Epsilon)
{
break;
}
// Generate new population by reproducing specimens from the selection
Current_Population =
SpecimenWorkshop.GeneratePopulation(Current_Selection);
Current_Iteration++;
}
}
}
}
// Class that implements the Genetic Algorithm
// at the highest abstraction level
public class Solver
{
// Current Population
static Specimen[] Current_Population;
// Current Selection
static Specimen[] Current_Selection;
// Current Proximity
static double Current_Proximity;
// Current Iteration
static int Current_Iteration;
// Initialize the algorithm
public static void Initialize()
{
// Set up options for the algorithm
SpecimenWorkshop.PopulationSize = 10000;
SpecimenWorkshop.SelectionSize = 1000;
SpecimenWorkshop.MaxMutationPercent = 500;
SpecimenWorkshop.MutationLikelyhoodPercent = 90;
SpecimenWorkshop.Epsilon = 0.000000001;
// Generate initial population
Current_Population = SpecimenWorkshop.GeneratePopulation();
Current_Iteration = 0;
}
// Run the algorithm
public static void Run()
{
// Set Current_Proximity to the biggest value
Current_Proximity = Double.MAX_VALUE;
// Loop while Current_Proximity is not less than the Epsilon
while (SpecimenWorkshop.Epsilon <= Current_Proximity)
{
// Select the best specimens
Current_Selection = SpecimenWorkshop.Select(Current_Population);
// Calculate proximity for the top-selected (the best) specimen
Current_Proximity = Current_Selection[0].Affinity;
// Report proximity and found solution for the current iteration
System.out.println(
Current_Iteration + "\t" +
Current_Proximity + "\t" +
Current_Selection[0].Genes[0]);
// End the calculations if Current_Proximity is less than the Epsilon
if (Current_Proximity < SpecimenWorkshop.Epsilon)
{
break;
}
// Generate new population by reproducing specimens from the selection
Current_Population = SpecimenWorkshop.GeneratePopulation(Current_Selection);
Current_Iteration++;
}
}
}
#ifndef H_SOLVER
#define H_SOLVER
#include "Specimen.h"
// Class that implements the Genetic Algorithm
// at the highest abstraction level
class Solver
{
public:
// Current Population
static Specimen** Current_Population;
// Current Selection
static Specimen** Current_Selection;
// Current Proximity
static double Current_Proximity;
// Current Iteration
static int Current_Iteration;
static void Solver::Initialize();
static void Solver::Run();
};
#endif
#include "Solver.h"
#include "SpecimenWorkshop.h"
#include <iostream>
// Current Population
Specimen** Solver::Current_Population;
// Current Selection
Specimen** Solver::Current_Selection;
// Current Proximity
double Solver::Current_Proximity;
// Current Iteration
int Solver::Current_Iteration;
// Initialize the algorithm
void Solver::Initialize()
{
// Set up options for the algorithm
SpecimenWorkshop::PopulationSize = 10000;
SpecimenWorkshop::SelectionSize = 1000;
SpecimenWorkshop::MaxMutationPercent = 500;
SpecimenWorkshop::MutationLikelyhoodPercent = 90;
SpecimenWorkshop::Epsilon = 0.000000001;
// Generate initial population
Current_Population = SpecimenWorkshop::GeneratePopulation();
// Set Current_Selection to zero for correct work delete[] operator
Current_Selection = 0;
Current_Iteration = 0;
}
// Run the algorithm
void Solver::Run()
{
// Set Current_Proximity to the biggest value
Current_Proximity = 1.7976931348623157E+308;
// Loop while Current_Proximity is not less than the Epsilon
while (SpecimenWorkshop::Epsilon <= Current_Proximity)
{
// Delete old selection
if (Current_Selection != 0)
{
for (int i = 0; i < SpecimenWorkshop::SelectionSize; i++)
{
// Calculate Affinity for new specimens
delete Current_Selection[i];
}
delete[] Current_Selection;
}
// Select the best specimens
Current_Selection = SpecimenWorkshop::Select(Current_Population);
// Calculate proximity for the top-selected (the best) specimen
Current_Proximity = Current_Selection[0]->Affinity;
// Report proximity and found solution for the current iteration
std::cout << Current_Iteration << '\t';
std::cout << Current_Proximity << '\t';
std::cout << Current_Selection[0]->Genes[0] << '\t' << std::endl;;
// End the calculations if Current_Proximity is less than the Epsilon
if (Current_Proximity < SpecimenWorkshop::Epsilon)
{
break;
}
// Delete old population
for (int i = 0; i < SpecimenWorkshop::PopulationSize; i++)
{
// Calculate Affinity for new specimens
delete Current_Population[i];
}
delete[] Current_Population;
// Generate new population by reproducing specimens from the selection
Current_Population = SpecimenWorkshop::GeneratePopulation(Current_Selection);
Current_Iteration++;
}
}
</iostream>
Main 方法
方法 Main
非常简单。它只包含调用 Solver
类的相关方法以及按下 **Enter** 键后关闭控制台窗口的命令。
using System;
namespace L2L_I2GA
{
class Program
{
static void Main(string[] args)
{
Solver.Initialize();
Solver.Run();
Console.WriteLine("Press Enter to exit.");
Console.ReadLine();
}
}
}
import java.io.IOException;
public class Program {
public static void main(String[] args) throws IOException {
Solver.Initialize();
Solver.Run();
System.out.println("Press Enter to exit.");
System.in.read();
}
}
#include <iostream>
#include "Solver.h"
int main()
{
Solver::Initialize();
Solver::Run();
std::cout << "Press Enter to exit." << std::endl;
std::cin.ignore(100000, '\n');
return 0;
}
</iostream>
输出
遗传算法包含许多随机操作。因此,每次运行的输出都会不同。一次运行的输出如下所示。
可能的缺点
遗传算法包含模糊和随机计算。尽管它可以解决非常困难的问题,但它可能不稳定并陷入无限循环。本章“输出”中显示的图片表明,迭代 [1, 2, 3]、[7, 8]、[9, 10]、[13, 14]、[17, 18]、[19, 20] 和 [25, 26] 包含相同的最佳解决方案(看起来像是复制品)。这表明在某些情况下,繁殖和突变没有产生任何比现有个体更好的个体,也没有使过程更接近解决方案。换句话说,27 个繁殖种群中有 8 个是无用的,并且处理这些种群的计算时间被浪费了。
通过更改 Class Solver
的 Initialize()
方法中的算法选项,读者可以研究示例的不同工作模式。例如,减小 PopulationSize
和 SelectionSize
,以及减小 MaxMutationPercent
和 MutationLikelyhoodPercent
,可能会导致算法陷入无限循环。
幸运的是,可以通过尝试运行示例并观察其工作原理来设置选项值。其他实际问题(其他方程等)可能更容易或更难解决,并需要相应地选择其他选项。也存在一些无法解决的问题。遗传算法非常灵活,可以适应以更好地解决特定类型的问题(请参阅我的另一篇文章“展望未来 - 机器人自动生成源代码”)。
可以使用数学方法来评估特定问题的可解性并为此算法进行调整。但这超出了本文的范围。
家庭作业
本章的标题可能会引起一些笑声。我只是借用了教育领域的一个术语。但本章可以看作是提醒读者,他们不仅可以阅读文本并运行示例,还可以思考“兔子洞有多深”并尝试自己进行一些额外的研究。
首先,读者可以回顾并找出源代码与生物过程之间的等同之处。作者已经描述了主要的理念,但生物过程和遗传算法本身非常多样且深刻,可以从中发现一些未提及的有趣之处。
其次,读者可以尝试使用不同的选项和不同的方程来运行源代码。这样的练习有助于他获得实践经验。
那些在编程方面有专长的人可以尝试其他繁殖和突变策略。他们还可以尝试解决具有多个变量的方程组,换句话说,具有多个基因(示例使用基因数组,提供了一定的可扩展性)。
我的文章“展望未来 - 机器人自动生成源代码”(在新窗口中打开)描述了使用遗传算法根据要求自动生成新函数。尽管本文看起来相当高级,但它展示了遗传算法的一些有趣能力。
历史
- 2018 年 12 月 15 日:首次发布