Genetics Dot Net - 基础知识






4.61/5 (24投票s)
2005年11月2日
30分钟阅读

97069

1853
遗传算法介绍。
引言
本系列文章旨在介绍遗传算法的世界,从最基本的开始,随着系列的推进,逐步深入到更复杂的解决方案。提供的代码旨在 Windows XP 上运行,使用 .NET 1.1 版本。本文将重点关注遗传算法的最基本元素,着重阐述什么是遗传算法,并提供一个演示应用程序的代码。尽管该应用程序存在某些问题,但将用于展示遗传算法如何通过更高级的方法工作,这些方法将是未来文章的目标。文章的次要目标是开发和提供一个免费的遗传库,允许我和其他人未来更容易地开发遗传算法,通过提供一套通用工具,使未来的开发者在使用该库时能够更专注于他们希望遗传算法做什么的应用程序特定方面,而不是不断地尝试解决如何做的问题。
什么是遗传算法?
遗传算法的理论基于生物学中繁殖和遗传的理念。简单来说,就是取一对字符串或字母或化学类型列表,它们是兼容的,然后混合和匹配它们,看看结果如何。当然,在生物学中,可以混合和匹配的范围是有限的,我们的遗传算法也应该如此。如果暂时将遗传算法简化到傻瓜级别,如果你有一杯咖啡、一些糖和一些盐,并尝试从这三者中选择两种进行不同组合,那么大多数人都会同意咖啡和糖的混合味道最好,而其他组合在提供提神饮料方面则差强人意。
遗传算法的生物学思想是 DNA 链,DNA 链的每个部分都等同于身体的特定功能或特征。尽管在此处可能应该提及,这种身体特征很少像高矮、大鼻子或小鼻子那样简单,而是这些事物更多是遗传链中独立化学物质影响的组合结果,这些影响我敢肯定还没有被任何人完全理解,所以不要指望基因整形手术很快就会出现在超市里。但是,我们姑且假设这种事情确实可能发生,我们可以跑到超市买到我们自己的药片形式的基因整形手术。它会如何构成呢?嗯,我猜大概是这样的:
你从几个基本类型中选择,从以下开始:
大小
Fat
Skinny
Medium
然后你考虑长度。
长度
Long
Short
Medium
最后,你给它一些特征。
特性
Hooked
Stubby
Average
你从每个部分拿一个药丸,早上醒来时就拥有了你选择的鼻子,如果你不喜欢,第二天晚上你总可以尝试另一种组合。当然,这里唯一的问题是,当你的基因构成决定时,你只是你母亲子宫里的化学汤,所以你影响结果的能力是有限的,再加上你根本不可能知道二十年后哪种鼻子会“流行”。所以和我们其他人一样,你必须让大自然发挥作用,然后在青少年时期抱怨它。
遗传算法的理念是模拟大自然在做出此类选择时所做的工作,并用它来寻找问题的答案。作为程序员,我们想要找到正确的答案,但我们现在不会过多担心这个问题,而只是专注于我们如何才能获得一个答案。要将遗传算法用于上述问题,我们所做的就是将解决方案表示为一个数组,然后从该数组中选择答案,这样我们就会有:
Array 0 :- SIZE
Array 1 :- LENGTH
Array 2 :- CHARACTERISTIC
数组中的每个项目都将包含另一个包含选项的数组,因此数组0将包含
array 0 :- Fat
array 1 :- Skinny
array 2 :- Medium
现在,如果在代码中我们用0和1来表示选项的开或关,其中1表示开,0表示关,那么一个胖鼻子的数组0输出将是100,一个瘦鼻子的输出将是010,一个中等鼻子的输出将是001。当然,我们也可以有混合和变体,例如一个中等胖的鼻子,它将是101,但我们暂时不深入探讨。因此,以此类推,一个完全普通的鼻子的输出将是
Array 0 :- 001
Array 1 :- 001
Array 2 :- 001
这将给我们一个完整的字符串(这里最好避免使用“鼻子字符串”这个词)001001001。现在,如果我们说这就是所有鼻子的定义方式,那么我们所需要做的就是操作这个字符串,我们只需将字符串中的值在1和0之间切换,就可以根据我们的需求选择任何可能的鼻子类型。
这当然就是遗传算法的作用,字符串不一定必须是0和1,它可以代表任何你想要它代表的东西,但以其最基本的形式,遗传算法会操纵一个字符串,以便程序从中获取字符串所代表的任何含义。字符串的含义和表示当然取决于程序员和项目需求。所以下一个问题是我们如何实现它。
演示代码
在我们深入了解如何实现遗传算法的细节之前,我们应该快速浏览一下文章中提供的演示代码。
对于这个项目,所有代码都包含在一个项目中,该项目会构建略有不同的可执行文件,旨在突出每篇文章的重点。此项目的演示代码是 GeneticsDev 项目,可以通过在 Developer Studio 中左键单击该项目,然后右键单击并选择菜单中的“设为启动项目”来构建。
演示代码的目的是提供一个看起来像二进制计算器的东西,即你在提供的框中输入一个要计算的和,代码尝试以二进制形式找到该和的答案。可以计算的二进制数字的数量可以通过代码进行配置,我们稍后将探讨其原因和影响。不过,目前 GeneticsDev 演示使用十位二进制数,因此任何返回1的数将返回字符串0000000001,2将是0000000010,3将是0000000011等等。请注意,这里没有真正使用二进制字符串的理由,我只是觉得它能简单地可视化代码中发生的情况。
演示本身是一个简单的前端,使用了三个自定义线程,一个在启动时触发,用于遗传算法的初始化,另外两个独立的线程用于执行计算。使用独立线程的原因是应用程序是处理器密集型的,如果不使用线程会变得无响应。
繁殖
遗传算法基本上是随机数据通过本质上是猜测的工作来寻找问题的答案或解决方案。初始数组是随机初始化的,然后我们希望将其推向答案或解决方案的方向。这当然要求我们至少对我们正在寻找的解决方案有所了解,但这并不意味着测试适应度的要求必须像我在本项目演示代码中显示的那样精确。基本上,可以编写一个适应度函数,要求代码实现一个目标,例如在地图中找到一条路径,而不需要代码找到地图中的特定路径,例如最短或最长的路径。然而,如果代码中根本没有任何适应度函数,我们只是在随风撒尿,希望能把它弄进一个随机放在我们身后的桶里。如果没有尝试引导程序找到我们正在寻找的解决方案,我们还不如大量调用随机函数,看看代码会做什么。这就是遗传算法版本一的作用。
所以我们取出随机初始化的数组,检查它是否包含正确答案,同时一直希望它不会在我们每次运行它时都开始随机猜测,对于那些已经运行过演示代码的人,你们可能已经看到有时确实如此。现在我们面临的情况是,我们有一个随机数据数组和一个我们希望代码找到的解决方案,遗传算法的做法是从数组中选择数据并用它来创建新数据。这是通过使用生物学技术,以某种方式组合数据来完成的,这样我们就可以从我们选择的数据中创建全新的解决方案尝试。这被称为繁殖。
繁殖是任何遗传算法的基石,其作用与生物学中完全相同。其思想是,你取两个或更多的亲代并繁衍后代,在这种情况下,我们使用二进制字符串,所以假设0000000001和0000000010是亲代,只能产生三个明显不同的后代,分别是0000000001、0000000010和0000000011,这些在十进制数字中分别为1、2和3。决定选择哪个后代的代码将在下面的“交叉”部分中介绍,现在我们只关注如何决定谁是亲代。
在遗传算法中选择亲代的通用术语叫做“适应度”。适应度旨在作为从其中选择遗传算法后代的亲代的方式,因此是遗传算法中最难讨论的部分,因为它总是完全应用程序特定的,所以我们将根据演示代码进行讨论,理解所选择的方法是完全可选的,并且即使在演示代码中也有其他选择亲代的方法。在选择遗传算法的适应度时,唯一的限制实际上是开发者的想象力和对项目需求的理解。
在演示项目中,我们设置了一个包含一百个二进制数的数组,这些二进制数的十进制值可能等于或不等于答案。如果没有任何二进制数的十进制值等于所需答案,那么我们从当前一代中创建新一代二进制数,但在此之前,我们需要从一百个数组中选择要作为亲代来产生后代的项。在本项目的演示代码中,位于 GeneticsDev 项目的 form one 中,我编写了一个名为 Fitness
的函数,该函数会检查数字数组,查看答案两侧是否有数字。如果有,代码将取正确答案两侧的二十五个值或任意数量的值。如果数组中的所有值都不大于所需答案,代码将取低于所需答案的二十五个值;如果没有值小于所需答案,则代码将取高于所需答案的二十五个值。Fitness
函数由遗传算法的第二版调用。
选择
在遗传算法做出任何决定之前,它必须选择谁将成为亲代,而且与自然界一样,孩子们可能并不总是得到他们应得的亲代。算法中为下一个孩子生成亲代有多种方法,我们将在稍后详细介绍,但在第一个演示中,使用了简单的随机算法,称为轮盘赌选择。轮盘赌选择只是从数组中提供的亲代中随机选择两个亲代,尽管这些亲代本身已经经过了适应度函数,因此从这个意义上说它并不是完全随机的。
交叉
交叉是遗传算法选择父代部分的方式,这可以是应用程序特定的,但在这些项目中始终保持通用。回到生物学类比,交叉是代码中一个父代有棕色眼睛,另一个父代有蓝色眼睛,然后决定孩子眼睛颜色的部分。有多种方法可以实现这一点,其中一些将在本项目的第二部分中讨论。不过目前,交叉过程大致如下。如果你有两个父代,0101010101 和 1010101010。你想创建一个混合了这两个父代的子代。最简单的方法,也是本项目演示代码中使用的方法,就是将父代的字符串拆分,然后简单地交叉它们,这样你就会得到一个看起来像:0101001010 的子代。
这为您提供了一个完全由父母遗传构成组成的独特后代。
变异
突变是将随机变异引入遗传字符串的过程,继续用自然类比来说,假设你有一个表示眼睛的字符串,并且该字符串的一部分有一个值来指示孩子的眼睛是否斜视。如果在创建孩子时生成一个随机值,并且该值小于一个给定值(通常非常低,在演示代码中设置为0.01),那么斜视眼睛的值将从其当前值更改为其相反值。因此,如果孩子斜视眼睛的值为0表示“不斜视”,当该值触发时,它将设置为1,孩子将有斜视眼睛。
代码
这个演示的代码旨在简单地展现遗传算法的世界,展示基本代码如何工作,并着重于演示事物的顺序,而不是从一开始就尝试创建一个完全可用的程序。我发现最好谈谈使用遗传算法时可能遇到的常见陷阱和问题,原因有二:首先,我们这里处理的不是一门精确的科学,每个项目都会不同,一个项目适用的解决方案很可能会完全搞砸另一个项目,所以在开始任何项目之前,理解其原因至关重要。
演示代码
演示项目是一个简单的Windows应用程序,它使用一个RichTextBox
和几个线程来初始化数组,并在应用程序运行时保持应用程序的可用性。代码本身是一种模拟计算器,可以进行用户可以输入的小额计算。数字随后被转换为二进制字符串。在这种情况下,它们是十个字符长,这样做的原因是,在测试中,十个字符在代码能够随机找到结果和能够通过算法确定结果之间提供了一个很好的平衡,而无需通过额外的检查和平衡来进一步复杂化代码。
代码使用这些遗传字符串来获取一个数字,然后尝试通过将数组中的二进制值与输入的和给出的值进行比较,来找到输入的和的正确答案。
应用程序启动时,会启动一个线程来初始化包含二进制数的数组,并且在初始化完成之前,用户界面会被禁用。然后,用户通过表单上的数字复选框输入一个总和。表单上的两个值受到限制,因为答案的总值不能超过一个十字符二进制数组所能容纳的最大值。用户输入总和后,可以通过两种遗传算法实现之一来运行它,第一种实现是一个非常基本的实现,依赖于随机数的生成;第二种实现则使用适应度函数来尝试引导算法找到正确答案。
在探讨这些实现目前存在的问题之前,让我们先了解它们是如何工作的。首先,在构造函数中,当名为 binGenetics
的变量作为 Binary Genetics 类的对象初始化时,数组也随之初始化。一旦初始化线程启动,就会调用 binGenetics.Initialise()
函数并运行以下代码:
for( int i=0; i<PopulationSize; i++ )
{
this.GeneticsArray.Add(new BinaryGeneticsString(ChromosomeLength));
}
这将一个 BinaryGeneticsString
对象(其 ChromosomeLength
为10,由类中的常量 SIZEOFBITS
设置)添加到了 GeneticsArray
中。BinaryGeneticsString
构造函数会调用其 InitialiseString
函数,用以下代码设置数字的二进制字符串:
Random rand = new Random();
for( int i=0; i<ChromosomeLength; i++ )
{
/// need to slow the computer down
///
System.Threading.Thread.Sleep( 2 );
GeneticsString.Add( rand.Next() % 2 );
}
它在 ChromosomeLength
大小的数组中创建了一个由零和一组成的随机字符串。一旦数组初始化完成,代码的用户界面就会激活,允许用户选择要运行的遗传算法版本。
从 GeneticsDev 项目中 *form1.cs* 文件中的 GeneticsThreadOne
函数开始,我们可以看到代码是如何运行的。不过需要提及的是,标记为 binTest
和 binAnswer
的变量是 BinaryConversion
类的对象,该类用于在十进制和二进制字符串之间进行转换,并在转换过程中内部存储数字。
当代码只是简单地将信息打印到 RichEditBox
时,它被替换为“/// print out”。
bool bStop = false;
int nRunCount = 0;
while( bStop == false )
{
/// print out
for( int i=0; i<binGenetics.PopulationSize; i++ )
{
binTest.BinaryString =
((BinaryGeneticsString)binGenetics.GeneticsArray[i]).ToString();
/// print out
if( binAnswer.Number == binTest.Number )
{
/// print out
bStop = true;
OnStopThreadOne( this, null );
break;
}
}
/// now update the gentics array
///
if( bStop == false )
{
binGenetics.Run();
nRunCount++;
}
}
代码遍历存储在 GeneticsArray
中的种群,并检查是否有任何值等于我们正在寻找的答案,如果找到答案,代码会停止线程并重置用户界面。如果在种群中未找到答案,则调用 BinaryGenetics
的 run 函数。
BinaryGenetics
的 run 函数是遗传学库中代码的入口,它看起来像这样:
ArrayList holdingList = new ArrayList();
for( int i=0; i<PopulationSize; i++ )
{
BinaryGeneticsString bsTemp =
new BinaryGeneticsString( ChromosomeLength, false );
RouletteWheelSelection();
SinglePointCrossOver( bsTemp, nCrossOverPoint );
holdingList.Add( bsTemp );
}
GeneticsArray.Clear();
for( int i=0; i<PopulationSize; i++ )
{
GeneticsArray.Add( holdingList[ i ] );
}
Mutation();
该代码展示了遗传算法函数被调用的顺序。首先,创建一个临时 ArrayList
来保存算法的新种群,然后初始化交叉点,这里将其设置在数组的中间位置。然后我们创建一个新的 BinaryGeneticsString
类型对象,该对象包含 GeneticsArray
种群的一个成员,在这种情况下,它将是进入算法下一代的子代。创建时传入 ChromosomeLength
(即遗传字符串的大小,在本例中为十个字符的数组)和一个初始化布尔值 false
,这表示构造函数不需要用随机字符填充数组,因为我们打算将对象传递给 SinglePointCrossover
函数,该函数将从 RouletteWheelSelection
函数选择的亲代(稍后讨论)中填充数组。然后将子代添加到临时数组中,并重复此过程,直到我们拥有遗传算法的新一代。然后将这一新一代从临时数组复制到主 GeneticsArray
中。
库
如上所述,该库的目的是为未来构建遗传算法项目提供一个框架,并作为模板来保存一些更通用的功能。在这种情况下,这意味着保存一些选择和交叉的功能,以便在测试算法时,能够相对简单地更改项目中使用的选择和交叉函数,从而找到能获得最佳结果的实现。主要的库类是 Genetics Base 类和 Genetics String 类,两者都旨在由当前的实现继承,以提供遗传算法的基本功能,以便继承它们的类可以专注于正确实现。
主类是 Genetics Base 类,它包含了遗传算法功能的主要蓝图,以及实现算法所需的变量,例如种群大小和染色体数组的长度。然而,其主要特点在于三个抽象函数以及它们如何由子类实现。这些是:
///
/// Initialise function for setting up the starting array
///
public abstract void Initialise();
///
/// Mutation function to add mutations where applicable
/// Note this will always be program specific
///
public abstract void Mutation();
///
/// Do a single run through the population of the genetics array
///
public abstract void Run();
Run
函数在子类 BinaryGenetics
中的实现如上所示,而 Mutation
函数将在下面的突变部分列出。Initialise
函数只是设置程序所需的遗传字符串。在 BinaryGenetics
程序中,它就是
for( int i=0; i<PopulationSize; i++ )
{
this.GeneticsArray.Add(new BinaryGeneticsString(ChromosomeLength));
}
它将一个新的 BinaryGeneticString
(它是该对象的子对象)添加到 GeneticsArray
中。GeneticsString
类在其最基本的层面上是一个简单的对象数组持有者,它持有将用作程序的遗传数据。它不必是字符串,可以是项目所需的任何类型的对象。该类有一个函数需要在子类中实现,那就是 InitialiseString
函数,它设置遗传字符串的每个部分,并且当然是高度应用程序特定的。BinaryGeneticsString
中的 InitialiseString
函数看起来像这样:
Random rand = new Random();
for( int i=0; i<ChromosomeLength; i++ )
{
/// need to slow the computer down
///
System.Threading.Thread.Sleep( 2 );
GeneticsString.Add( rand.Next() % 2 );
}
这里重要的行是 GeneticsString.Add
函数,它将染色体长度的 0 或 1 添加到 GeneticsArray
。
适应度
如前所述,遗传算法适应度的计算方式是高度应用程序特定的,所以我在这里只能结合当前项目来讨论。Fitness
函数的理念是,我们希望缩小数组中的值范围,使其接近我们正在寻找的值。在演示项目中,如果我们有一个答案54,并且有一个由随机生成的数字组成的数组,我们正试图从中找到答案,我们自然希望从数组中移除那些离我们所需值最远的答案。在项目中,一旦我们确定没有正确答案,就会通过获取最接近正确答案的二十五个值来完成此操作。Fitness
函数的代码太长,无法在此处列出,可以在 *Form1.cs* 中找到。
选择
如上所述,第一个演示项目使用的选择方法是轮盘赌选择法,代码如下:
Random rand = new Random();
int nSelected = rand.Next( GeneticsArray.Count );
gsParentOne = ( GeneticsStrings )GeneticsArray[ nSelected ];
System.Threading.Thread.Sleep( 2 );
nSelected = rand.Next( GeneticsArray.Count );
while( gsParentOne ==
( GeneticsStrings )GeneticsArray[ nSelected ] )
{
nSelected = rand.Next( GeneticsArray.Count );
}
gsParentTwo = ( GeneticsStrings )GeneticsArray[ nSelected ];
变量 gsParentOne
和 gsParentTwo
是 GeneticsBase
类中的 private
GeneticsString
类成员,它们用于存储将由交叉函数创建的新子代的亲代。从上面的代码可以看出,这是通过从数组中随机选择一个字符串,然后选择另一个字符串,同时确保我们选择了两个不同的亲代来完成的。
交叉
一旦我们选择了将组成子代的父母,我们需要进入交叉函数,其功能是取一个父母的一部分和另一个父母的一部分,并将它们混合在一起,这样我们就得到一个新的,在这种情况下是二进制字符串
child.GeneticsString.Clear();
for( int i=0; i<SingleCrossOverPoint; i++ )
{
child.GeneticsString.Add( gsParentOne.GeneticsString[ i ] );
}
for( int i=SingleCrossOverPoint; i<gsParentTwo.GeneticsString.Count; i++ )
{
child.GeneticsString.Add( gsParentTwo.GeneticsString[ i ] );
}
与选择函数一样,有几种交叉函数可供我们使用,这里使用的是单点交叉。SinglePointCrossOver
函数在 GeneticsString
内的一个单点处连接父母,该点由 GeneticsBase
类中的 SingleCrossOverPoint
值设置。
变异
这个过程的最后一部分是变异部分,至少这是库的组织方式,它在完整的子代数组构建完成后才起作用,而不是在每个子代创建时单独应用。它也不在基类中应用,因为再次强调它是应用程序特定的,你无法为遗传算法编写变异函数,直到你知道你正在变异什么。所以该函数可以在 BinaryGenetics
类中找到,它看起来像这样:
Random rand = new Random();
Random randLocation = new Random();
int nLocation = 0;
BinaryGeneticsString bsTemp = null;
for( int i=0; i<PopulationSize; i++ )
{
if( rand.NextDouble() < MutationRate )
{
nLocation = randLocation.Next( ChromosomeLength );
bsTemp = ( BinaryGeneticsString )GeneticsArray[ i ];
if( ( Int32 )bsTemp.GeneticsString[ nLocation ] == 0 )
{
bsTemp.GeneticsString[ nLocation ] = 1;
}
else
bsTemp.GeneticsString[ nLocation ] = 0;
}
}
一个孩子是否会发生变异完全取决于 MutationRate
,该值可调,但在 GeneticsBase
类中被设置为 0.01,这使得它被使用的机会非常小。然而,如果碰巧发生了变异,我们会在孩子字符串中选择一个随机位置,然后简单地翻转我们找到的任何位。
问题
现在在一个完美的世界里,我们就能说:好了,收工,收拾东西回家,打开魔兽世界,然后把脚放上去。不幸的是,还有一个小小的挫折,那就是我们编写的程序有一个微小但明显的缺陷。这个小缺陷就是它根本不擅长做我们想让它做的事情。事实上,如果你运行第二个算法足够长的时间,你就会得到这样的结果:
解决问题
这里的问题是,尽管算法已经非常接近答案,毕竟只差12,但算法不够复杂,无法
- 检测到它接近但不足以回答,并且,
- 它没有办法改变自己来纠正这个问题。
解决此问题的一个快速方法是 GeneticsBase
类中的 SingleCrossOverPoint
变量,该变量设置 SinglePointCrossOver
算法将父代字符串复制到新创建的子代字符串的点。当此值设置为四时,数字将趋向于收敛到接近该值,但当设置为五时,它们似乎距离更远。
然而,这是遗传算法的一个常见问题,虽然改变交叉点或完全改变算法是一种解决方案,但并没有保证的解决方案。我们有一些工具可以使用。这里是其中两个。
在 *Form1.cs* 文件的第49行,有一个名为 bool bProblemSolving
的变量,默认设置为 false
。如果您在编译之前将其设置为 true
,您将看到程序界面发生如下变化:
世代
一旦点击了世代复选框,代码将使用默认设置或对默认设置进行的任何更改。它的工作原理是,每个世代需要五十个繁殖周期来创建。如果在一个世代结束时没有找到总和的答案(在这种情况下),那么遗传数组会被重新初始化,并再次开始这个过程,直到找到正确答案或程序运行完所有分配的世代。将世代代码添加到程序中相当简单,我们只需初始化世代代码,然后用更新代码中断循环即可。
要初始化世代,我们添加以下代码:
if( bProblemSolving == true )
{
binGenetics.UseGenerations = generationsBox.Checked;
if( binGenetics.UseGenerations == true )
{
binGenetics.NumberOfGenerations =
( int )numberGenerationsBox.Value;
binGenetics.NumberOfCyclesPerGeneration =
( int )cyclesPerGenerationBox.Value;
}
}
它检查问题解决模式是否开启,然后检查世代选项是否被选中,如果选中,则控制世代代码的变量将被设置为数字框中的值。然后将以下代码添加到循环中以控制程序的流程:
if( binGenetics.UseGenerations == true )
{
if( nGenerationCount == binGenetics.NumberOfGenerations )
{
/// output text
bStop = true;
OnStopThreadOne( this, null );
}
if( nRunCount == binGenetics.NumberOfCyclesPerGeneration )
{
/// output text
binGenetics.Initialise();
nGenerationCount++;
nRunCount = 0;
}
}
与初始化代码一样,我们首先检查问题解决选项是否开启,然后检查我们是否已用完所有允许的世代,如果还没有,我们再检查是否已达到一个周期的末尾,如果达到了,我们就重置所有内容并重新初始化 GeneticsArray
以重新开始。
精英主义
精英主义是将 GeneticsArray
中的项目保留到数组的下一代的过程,这样一代中的父代将与下一代中的子代混合。在 GeneticsDev 示例中,代码保存了最接近我们正在寻找的答案的五个值。执行此操作的代码类似于检查 *form1.cs* 文件中适应度值的代码。实际上,它最初是直接剪切粘贴的,然后从 *BinaryGenetics.cs* 文件中的 AddEliteValues
函数进行了编辑。
将精英主义添加到 GeneticsBase
类的子类中其实很简单,你只需在运行函数中添加类似这样的代码:
if( UseElitism == true )
{
AddEliteValues();
}
简单检查精英主义是否正在使用,然后调用您需要编写的函数来获取您希望保存的精英值。当然,您必须先进行设置,这需要
binGenetics.UseElitism = elitismBox.Checked;
if( binGenetics.UseElitism == true )
{
binGenetics.ElitismValue = binAnswer.Number;
}
或类似的将在调用 run 函数的函数中添加的代码。
附加代码
随着任何库的开发,都需要相应的功能,所以我还包含了一些选择和交叉方法,这些方法可以在使用该库开发的任何应用程序中使用。为了开发和测试这些方法,我决定不将它们硬塞进 GeneticsDev 应用程序,而是包含一个专门为此目的开发的第二个应用程序。这就是随示例代码提供的 GeneticsDevAdditional 应用程序。要编译并运行此示例,请右键单击 GeneticsDevAdditional 标题并选择“设为启动项目”。
正如您所见,为了容纳两个标签页上额外的选项,进行了一些外观上的更改,一个标签页包含选择选项,另一个标签页包含交叉选项。代码的主要更改是:
binGenetics.CrossOverMethod = CROSSOVERMETHODS.SINGLEPOINTCROSSOVER;
binGenetics.SelectionMethod = SELECTIONMETHODS.ROULETTEWHEEL;
/// This section will hold the changable values
/// for the genetics selection
/// and crossover.
///
binGenetics.SingleCrossOverPoint = 5;
binGenetics.DualPointCrossOverPointOne = 4;
binGenetics.DualPointCrossOverPointTwo = 7;
binGenetics.PositionBasedCrossOverPoints = 5;
在 GeneticsDevAdditional 项目的 *Form1.cs* 文件中,正如前两行所示,Crossover
和 Selection
方法现在通过基类中的 enum
设置,然后由 Selection
和 CrossOver
函数使用,并从 BinaryGenetics
类中修改后的 run 函数调用:
Selection();
CrossOver( bsTemp );
选择方法
选择只有两个选项。第一个是轮盘赌选项,我们之前已经见过;第二个是定点选择选项。从这段代码的角度来看,问题在于一旦你开始深入选择方法的具体细节,你就会进入应用程序特定代码的领域,所以这个选项的目的是让使用该库的人可以编写一个选择算法,然后简单地告诉基类它希望使用哪两个数组项作为亲代。
这里给出的代码只是简单地从数组中取出前两个项目,并将数组值传递给 Selection
方法,同时像往常一样调用 Crossover
方法。您可以通过多种其他方式实现这一点,例如,您可以从数组的两端开始向中间工作,或者,您可以扩展通过精英函数保存的数组项目数量,然后仅使用这些数组位置来为下一代生成子代。
交叉方法
有四种交叉方法可用。第一种是 SinglePointCrossOver
,它在原始应用程序中使用;然后是 RandomPointCrossOver
、DualPointCrossOver
和 PositionBasedCrossOver
。这些都是从父数组中选择值并将其复制到子数组中的变体。
RandomPointCrossOver
函数随机选择交叉点,并将父代1的值复制到第一部分,将父代2的值复制到第二部分。
DualPointCrossOver
函数在数组中选择两个点。两端用父代1填充,中间用父代2填充。
PositionBasedCrossOver
与 RandomPointCrossOver
函数类似,不同之处在于整个父代1数组被复制到子代数组中,然后从父代2中随机选择固定数量的点,这些点再复制到子代数组的相同位置。
Selection
和 CrossOver
方法的代码都可以在 *GeneticsBase.cs* 文件中找到。
结论
遗传算法的初学者指南到此结束。在下一部分中,我们将探讨如何在更接近现实的应用程序中使用遗传算法,例如在已知目标但不知道如何实现目标的情况下,它能做些什么有用的事情,我将探讨自适应编程。
主要参考文献
- Mat Buckland,( 2002 ) 游戏编程中的 AI 技术,Premier Press 游戏开发系列。
- David E. Goldberg,( 1989 ) 搜索优化和机器学习中的遗传算法,Addison Wesley。
- Wolfgang Banzhaf 等,( 1998 ) 遗传编程入门,Morgan Kaufmann。
参考文献
- Tom Archer ( 2001 ) C# 内幕,Microsoft Press
- Jeffrey Richter ( 2002 ) 应用 Microsoft .NET 框架编程,Microsoft Press
- Charles Peltzold ( 2002 ) 使用 C# 编程 Microsoft Windows,Microsoft Press
- Robinson 等 ( 2001 ) 专业 C#,Wrox
- William R. Staneck ( 1997 ) Web 发布 Unleashed 专业参考版,Sams.net
- Robert Callan,( 1999 ) 神经网络的本质,Prentice Hall
- Timothy Masters ( 1993 ) C++ 中的实用神经网络食谱,Morgan Kaufmann ( Academic Press )
- Melanie Mitchell ( 1999 ) 遗传算法导论,MIT Press
- Joey Rogers ( 1997 ) C++ 中的面向对象神经网络,Academic Press
- Simon Haykin ( 1999 ) 神经网络:全面基础,Prentice Hall
- Bernd Oestereich ( 2002 ) 使用 UML 开发软件:实践中的面向对象分析与设计,Addison Wesley
- R Beale & T Jackson ( 1990 ) 神经网络计算导论,Institute Of Physics Publishing
- Bart Kosko ( 1994 ) 模糊思维,Flamingo
- Buckley & Eslami ( 2002 ) 模糊逻辑与模糊集导论,Physica-Verlag
- Steven Johnson ( 2001 ) 涌现,The Penguin Press
- John H. Holland ( 1998 ) 从混沌到秩序的涌现,Oxford University Press
- Earl Cox ( 1999 ) 模糊系统手册,AP Professional
- Mark Ward ( 1999 ) 虚拟生物,Pan
- Bonabeau, Dorigo, Theraulaz ( 1999 ) 群体智能:从自然到人工系统,Oxford University Press
- Masao Mukaidono, 模糊逻辑入门 ( 2002 ) World Scientific Publishing
- Deborah Gordon, 忙碌的蚂蚁 ( 1999 ), W W Norton
- Gigerenzer, Todd & ABC 研究小组 ( 1999 ) 让我们更聪明的简单启发法,牛津大学出版社
- Rodney A. Brooks ( 1999 ) 寒武纪智能,MIT Press
- Winfred F. Hill, ( 1964 ) 学习:心理学解释综述,University Paperbacks。
- M. Tim Jones, ( 2003 ) AI 应用编程,Charles River Media