【转载】遗传算法的C#实现及应用

遗传算法的C#实现及应用
吴晓春

摘要
用C#语言来实现遗传算法的通用代码GA类,然后调动GA类解决组合优化领域的经典问题TSP,并给出了实现程序。

关键词
遗传算法,TSP问题

    遗传算法的历史起源可追溯至60年代,Holland于1975年出版的著名著作《自然系统和人工系统的适配》系统地阐述了遗传算法的基本理论和方法, 并提出了对遗传算法的理论研究和发展极为重要的模式理论。这一理论首次确认了结构重组遗传操作对于获得隐并行性的重要性。
    进入80年代,遗传算法迎来了兴盛发展时期, 理论和应用研究都成了十分热门的课题。

一、遗传算法的C#实现

    遗传算法是模拟生物遗传进化的过程, 在全局优化过程中找到最优解或其近似解的有效算法。简单地说, 让多个随机解不断地繁殖, 每一代解不断地进化, 最后得到最优解或近似最优解。解决某一问题的遗传算法的实现过程有以下几个步骤:
(1)初始:随机产生待解决问题的若干解,每个解称为染色体(又称个体),所有解称为群体。
(2)适应度:计算群体中每个染色体的适应度,代表解的优化程度。
(3)新一代群体:通过下列步骤的循环产生新一代的群体,称子代群体即新的若干个解。
1)选择:按照适应度选择两个父代染色体作为双亲染色体。适应度高,被选择的几率就高。
2)交叉:按照交叉率由双亲染色体交叉形成两个新一代染色体, 若交叉不能完成, 直接由双亲复制产生两个子代染色体。
3)变异:按照变异率让染色体的某个基因产生突变形成新的染色体。
4)替换:使用新的群体代替原群体。即新的若干个解代替原来的若干个解。
(4)测试:如果群体繁殖的次数已经足够, 返回群体中的适应度最优的个体作为解。
(5)循环:返回到第2步
    上述步骤中,群体初始化、适应度计算、选择、交叉、变异都与具体的问题紧紧联系,其代码受具体问题的制约。为了通用性,在实现遗传算法时,用代理代表这些功能。遗传算法的通用C#代码包括三个类,分别为基因类(GAGene)、染色体类(GAChromosome)、遗传算法类(GA)。
    基因类GAGene由构造方法和Value属性组成,Value是字串型,对应实际问题的最基本信息。染色体类GAChromosome继承ArrayList来实现,由若干基因顺序组成。遗传算法类GA实现遗传算法的总体功能。
    初始群体的产生是采用随机的方法产生问题的多个解,核心代码如下:
public void Initialize()
{
    try
    {
        if(this.EnableLogging && this.LogFilePath !=””)
            System.IO.File.Delete(LogFilePath);//染色体数据存盘
        if(this.EnableLogging)
        {
            AddToLogFile(“序号\t染色体\t适应度”);
            AddToLogFile(“———–“);
        }
    }
    catch(System.IO.IOException exp){}
    m_thisGeneration = new System.Collections.ArrayList();//当前群体
    for(int i = 0;i<PopulationSize;i++)//PopulationSize为群体大小,即染色体个数
    {
        GAChromosome newParent = new GAChromosome(m_int,m_fit,m_mutate);
        //利用m_int生成染色体,并用m_fit计算染色体的适应度
        //染色体变异时使用m_mutate
        m_thisGeneration.Add(newParent);//添加到群体中
        if(this.EnableLogging)
            AppendToLogFile(i,newParent);
        //数据存盘
    }
    RankPopulation();//根据适应度对染色体排序
}
    在构造染色体new GAChromosome(m_int,m_fit,m_mutate)时,通过三个参数的代理方法来生成染色体、计算适应度、实
现变异。具体代码随具体问题而不同。
    新一代群体,又称为子代群体,它是在父代群体中选择优秀的染色体经过交叉、变异产生的。为了保证子代群体比父代群体在整体上更为优秀,选择方法、交叉方法和变异方法都要根据具体问题来设计调整,基于子代比父代比更优秀的趋势,群体向着最优解靠近,通过不断的循环(繁殖),总能找到最优解或其近似解。核心代码如下:
public void CreateNextGeneration()//生成新一代群体

{

    m_NextGeneration.Clear();             

    GAChromosome bestChromo = null;

    if (this.ApplyElitism) // 最优秀(适度度最好)的染色体是否直接选择加入到新一代群体

        bestChromo = (GAChromosome)m_thisGeneration[0];//取出最优染色体(已排序)

    for (int i = 0; i < this.PopulationSize; i += 2)//逐步生成新一代群体,每次生成两个

    {

        //Step 1 选择:选择父代中的两个优秀的染色体

        int iDadParent = 0;

        int iMumParent = 0;

        switch (this.SelectionType) //选择的方法有:轮赌、竞争、等级等等

        {

            case Selection.Tournment: //竞争法

                iDadParent = TournamentSelection();

                iMumParent = TournamentSelection();

                break;

           case Selection.Roullette: //轮赌法

                iDadParent = RouletteSelection();

                iMumParent = RouletteSelection();                       

                break;

        }

        GAChromosome Dad = (GAChromosome)m_thisGeneration[iDadParent];

        GAChromosome Mum = (GAChromosome)m_thisGeneration[iMumParent];

        GAChromosome child1 = new GAChromosome(this.m_fit, this.m_mutate);

        GAChromosome child2 = new GAChromosome(this.m_fit, this.m_mutate);

        //Step 2  交叉:参照交叉率随机决定,是交叉产生两个下一代的染色体还是直接产生

        if (m_Random.NextDouble() < this.CrossOver)

        {  m_crossOver(Dad, Mum, ref  child1, ref child2); }

        else

        {   child1 = Dad;

            child2 = Mum;

        }

        //Step 3  变异:参照变异率决定是否变异

        if (m_Random.NextDouble() < this.Mutation)

        {   m_mutate(child1);

            m_mutate(child2);

        }

        //Step 4 计算适应度                    

        this.m_fit(child1);

        this.m_fit(child2);

        m_NextGeneration.Add(child1);

        m_NextGeneration.Add(child2);

    }//for  子代群体生成结束

    if (null != bestChromo)

        m_NextGeneration.Insert(0, bestChromo); //最优的染色体插入子代群体中

    m_thisGeneration.Clear(); //用新一代替换当代群体              

    for (int j = 0; j < this.PopulationSize; j++)

    { m_thisGeneration.Add(m_NextGeneration[j]);}

    this.RankPopulation();//按照适应度对染色体排序

    this.GenerationNum++;//优化过程计数

}
    优秀的染色体是指适应度高的染色体。群体中最优的那个染色体将毫无改变的复制到新的群体中。用比较优秀的染色体产生子代,才能保证子代更优秀,以达到全局优化的目的。选择优秀染色体的方法较多,如轮赌选择法、竞赛(或称竞争)选择法、分级选择法、稳定状态选择法等这里只介绍轮赌选择法和竞赛选择法。

    轮赌选择法:父代的选择是根据他们的适应度做出的。染色体适应度越高,那么它被选择到的机会就越大。想像一个轮盘赌的机器上放置了群体中所有的染色体。每一个染色体所占的地方的大小和它的适应度成正比。然后开始扔弹子,扔到那个地方就把对应的染色体选择出来。显然,适应度越大的染色体被选到的机会就越大。 这个过程可以用下面的这个算法来模拟:计算所有染色体的适应度之和S (大写);在区间(0,S)上随机的产生一个数r;在群体中从适应度最小的染色体开始,把它的适应度加到s (小写)上去(s开始为0),如果s大于r,则停止循环并返回当前染色体,核心代码如下:
private int RouletteSelection()//轮赌选择法
{
    double randomFitness = m_Random.NextDouble() * m_dTotalFitness;
    //在当中群体中找到适应度与randomFitness想接近的染色体
    int idx = -1;
    int i = 0;
    double sumofpart = m_dTotalFitness;
    int last = this.PopulationSize – 1;
    while(i<=last)
    {
        sumofpart = sumofpart – ((GAChromosome)this.m_thisGeneration[i]).Fitness;
        if(randomFitness>sumofpart)
        {idx = i; break;}
        i++;
    }   
    return idx;
}
竞赛选择法:从父代群体中任意选择一定数目的染色体,其中适应度最高的染色体选出。核心代码如下:
private int TournamentSelection()//竞争选择法
{
    //根据群体大小决定count,在群体中随机找count个染色体,其中适应度最大一个选出
    int Count = 1;
    if(this.PopulationSize >= 50)
        Count = 8;
    else if(this.PopulationSize >= 30)
        Count = 6;
    else if(this.PopulationSize >= 20)
        Count = 4;
    else if(this.PopulationSize >= 10)
        Count = 3;    
    else if(this.PopulationSize >= 2)
        Count = 2;
    int finalindex = 0;
    double dMaxFit = 0;
    for(int i = 0;i<Count;i++)
    //适应度最大一个染色体选出
    {
        int sellndex = m_Random.Next(0,this.PopulationSize);
        double fitness = ((GAChromosome)m_thisGeneration[sellndex]).Fitness;
        if(fitness > dMaxFit)
        {
            finalindex = i;
            dMaxFit = fitness;
        }
    }
    return finalindex;
}
    交叉是指把双亲染色体的部分结构加以替换重组而生成新的染色体的操作,它在遗传算法中起核心作用。通过交叉,父代中优秀染色体的性状能在子代中得到遗传和继承。主要的交叉算法有:
    (1)单交叉点交叉:选择一个交叉点,子代在交叉点前面的染色体从一个父代染色体那里得到,后面的部分从另外一个父代染色体那里得到。
    (2)双交叉点交叉:选择两个交叉点,子代染色体在两个交叉点间部分来自一个父代染色体,其余部分来自于另外一个父代染色体。
    (3)均匀交叉:子代染色体的每一个基因是随机地来自于两个父代染色体中的一个的。
    (4)算术交叉:对父代染色体做一个代数运算从而产生一个新的染色体,如与运算。
    遗传算法中,交叉操作因其全局优化的能力而作为主要的操作,变异操作因其局部优化能力而作为辅助操作。当遗传算法通过交叉操作接近最优解时,利用变异操作的这种局部随机能力可以加速向最优解收敛。变异概率应取较小值,防止接近最优解的状况因变异而遭到破坏。
    常见的变异操作有:
    (l)基本变异操作:对群体中的染色体随机挑选一个或多个基因根据变异概率作变动。
    (2)逆转变异操作:对群体中的染色体随机挑选二个逆转点,将其间的基因逆向排序。
    (3)自适应变异操作:与基本变异操作相似,不同的是变异概率随群体中染色体的多样性程度而自应调整。
    交叉操作和变异操作实现群体结构的重组和优化,其算法与所求解的具体问题有关,实现代码在应用部分给出。

二、巡回旅行商问题
    巡回旅行商问题(TSP),也称为货郎担问题:不重复地经过几个城市并回到起点,求出最短路线。TSP是一个具有广泛的应用背景和重要理论价值的组合优化问题,是组合优化领域的一个典型的NP难题。几十年来,出现了很多近似优化算法,如近邻法、贪心算法、最近插人法等。近年来,推出了很多有效的解决方法如神经网络方法、模拟退火方法及遗传算法等。
    TSP搜索空间随着城市数n的增加而增大,所有的旅程路线组合数为(n-1)!/2。5个城市的情形对应12条路线,10个城市的情形对181440条路线,100个城市的情形则对应有4.6663*10的155次方条路线。在如此庞大的搜索空间中寻求最优解,对于常规方法和现有的计算工具而言,存在着诸多的计算困难。借助遗传算法,可以有效地解决TSP问题。
    利用前述的遗传算法GA类解决TSP问题的核心代码如下:
private void btn_Run_Click(object sender,System.EventArgs e)
{
    ga.Initializer newItializer = this.Initializer;
    //向GA类传送四个代理方法
    ga.Mutate mutater = this.ChromoseCompraror;
    ga.Fitness fitmethod = this.Fitness;
    ga.CrossOver CrossMethod = this.CrossOver;
    ga.GA GAAlgo = new ga.GA(newItializer,fitmethod,mutater,CrossMethod);
    //定义GA对象,设置群体代数,群体大小,变异率,交叉率
    GAAlgo.Generation = long.Parse(this.num_Gnr.Value.ToString());
    GAAlgo.PopulationSize = ushort.Parse(this.num_PopSiz.Value.ToString());
    GAAlgo.Mutation = double.Parse(this.num_Mutation.Value.ToString());
    GAAlgo.CrossOver = double.Parse(this.numCO.Value.ToString());
    GAAlgo.Initialize();//产生第一代群体
    while(!GAAlgo.IsDone())//产生每一代群体
    GAAlgo.CreateNextGeneration();
    m_chro = GAAlgo.GetBestChromosome();
    //返回最优的染色体
    DrawCitiesPath(m_chro,Color.Navy);
    //在界面上划出最短路径
    System.Drawing.Graphics();
    //以下代码为显示优化路径过程,为每一代中最优染色体的适应度描出的曲线
    int i;
    for(i=0;i<GAAlgo.Generation*4-4;i+=4)
    objGraph.DrawLine((new Pen(Color.Red,2)),i+20,10*float.Parse(GAAlgo.linePoint[i/4+1].ToString()));
    objGraph.DrawString(“最短路径长度:”+(CitiesPanel.Height-10-10*float.Parse(GAAlao.linePoint[i/4].ToString())).Tostring),new System.Drawing.Font(“Arial”,10),System.Drawing.Brushes.Blue,i+24,10*float.Parse(GAAlgo.linePoint[i/4].ToString());
}
    利用GA类需要准备四个方法和四个参数。
    染色体生成方法:初始时,随机产生多个城市点,城市坐标依产生先后保存在全局变量Points中,把城市的序号作为基因,一条旅行路线上城市序号的排列就是一个染色体(即一个解),染色体生成方法的核心代码如下:
//产生一个染色体(即一个TSP路径)
private void Initializer(GAChromosome chromose)
{
    ArrayList tmpPoints = new ArrayList();
    for(int i = 0; i<Points.Count; i++)
        tmpPoints.Add(i);//城市序号顺序存入tmpPoints
    do
    {
        int numOne = RndObj.Next(0,tmpPoints.Count);
        //随机得到一个城市序号
        chromose.AddGene(new GAGene(tmpPoints[numOne].ToString()));//生成基因,加入染色体
        tmpPoints.RemoveAt(numOne);
        //删去该城市序号,避免再选
    }
    while(tmpPoints.Count>0);
}
    产生多条染色体,构成初始群体。染色体的适应度用染色体中相邻重复基因(城市)的距离的总和的倒数来表示,适应度计算方法为:private void Fitness(GAChromosome chromosome)。对群体中的每个染色体计算适应度并降序排序。选择两个优秀的染色体, 通过交叉、变异产生新一代的染色体。
    这里使用单点交叉法, 具体步骤如下:
    在父亲染色体中随机选一个基因;在母亲染色体中定位该基因;在孩子染色体中插入该基因作为第一个基因;在父亲染色体中依次从该基因向左选一个基因,若它未出现在孩子染色体中则左侧加人;在母亲染色体中依次从该基因向右选一个基因,若它未出现在孩子染色体中则右侧加入;当双亲染色体中的基因全加入到孩子染色体中后,孩子染色体的长度不足, 剩下的城市序号作为基因随机加入孩子染色体。
    比如Dad=D H B A C F G B Mum=B C D G H F E A
    孩子染色体的第一个基因是C,从Dad染色体中C的左侧选一个是A,A未出现在孩子染色体中,这样孩子染色体为AC;从Mum中C的右侧选一个是D,未出现在孩子染色体中,这样孩子染色体为ACD;不断重复。
    核心代码如下:

    原文作者:kaoyanmp3
    原文地址: https://www.cnblogs.com/kaoyanmp3/archive/2013/03/31/2991883.html
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞