10分钟搞懂遗传算法

这里写图片描述

大天然有种神奇的力量,它可以将优良的基因保留下来,从而进化出更增强大、更加适合生存的基因。遗传算法便基于达尔文的进化论,模拟了天然选择,物竞天择、适者生存,经过N代的遗传、变异、交叉、复制,进化出问题的最优解。遗传算法看似神奇,但实现思路却较为简单。本文先跟你们介绍遗传算法的基本思想,而后用遗传算法来解决一个实际问题,最后给出遗传算法的代码实现和解析。废话很少说,如今就开始吧~html

遗传算法

在开始以前,咱们先来了解下遗传算法中的几个概念。node

概念1:基因和染色体

在遗传算法中,咱们首先须要将要解决的问题映射成一个数学问题,也就是所谓的“数学建模”,那么这个问题的一个可行解即被称为一条“染色体”。一个可行解通常由多个元素构成,那么这每个元素就被称为染色体上的一个“基因”。git

好比说,对于以下函数而言,[1,2,3]、[1,3,2]、[3,2,1]均是这个函数的可行解(代进去成当即为可行解),那么这些可行解在遗传算法中均被称为染色体。程序员

3x+4y+5z<100github

这些可行解一共有三个元素构成,那么在遗传算法中,每一个元素就被称为组成染色体的一个基因。web

概念2:适应度函数

在天然界中,彷佛存在着一个上帝,它可以选择出每一代中比较优良的个体,而淘汰一些环境适应度较差的我的。那么在遗传算法中,如何衡量染色体的优劣呢?这就是由适应度函数完成的。适应度函数在遗传算法中扮演者这个“上帝”的角色。算法

遗传算法在运行的过程当中会进行N次迭代,每次迭代都会生成若干条染色体。适应度函数会给本次迭代中生成的全部染色体打个分,来评判这些染色体的适应度,而后将适应度较低的染色体淘汰掉,只保留适应度较高的染色体,从而通过若干次迭代后染色体的质量将愈来愈优良。数组

概念3:交叉

遗传算法每一次迭代都会生成N条染色体,在遗传算法中,这每一次迭代就被称为一次“进化”。那么,每次进化新生成的染色体是如何而来的呢?——答案就是“交叉”,你能够把它理解为交配。服务器

交叉的过程须要从上一代的染色体中寻找两条染色体,一条是爸爸,一条是妈妈。而后将这两条染色体的某一个位置切断,并拼接在一块儿,从而生成一条新的染色体。这条新染色体上即包含了必定数量的爸爸的基因,也包含了必定数量的妈妈的基因。负载均衡

遗传算法结果展现

那么,如何从上一代染色体中选出爸爸和妈妈的基因呢?这不是随机选择的,通常是经过轮盘赌算法完成。

在每完成一次进化后,都要计算每一条染色体的适应度,而后采用以下公式计算每一条染色体的适应度几率。那么在进行交叉过程时,就须要根据这个几率来选择父母染色体。适应度比较大的染色体被选中的几率就越高。这也就是为何遗传算法能保留优良基因的缘由。

染色体i被选择的几率 = 染色体i的适应度 / 全部染色体的适应度之和

概念4:变异

交叉能保证每次进化留下优良的基因,但它仅仅是对原有的结果集进行选择,基因仍是那么几个,只不过交换了他们的组合顺序。这只能保证通过N次进化后,计算结果更接近于局部最优解,而永远没办法达到全局最优解,为了解决这一个问题,咱们须要引入变异。

变异很好理解。当咱们经过交叉生成了一条新的染色体后,须要在新染色体上随机选择若干个基因,而后随机修改基因的值,从而给现有的染色体引入了新的基因,突破了当前搜索的限制,更有利于算法寻找到全局最优解。

概念5:复制

每次进化中,为了保留上一代优良的染色体,须要将上一代中适应度最高的几条染色体直接原封不动地复制给下一代。

假设每次进化都需生成N条染色体,那么每次进化中,经过交叉方式须要生成N-M条染色体,剩余的M条染色体经过复制上一代适应度最高的M条染色体而来。

遗传算法的流程

经过上述概念,相信遗传算法的大体原理你已经了解,下面咱们将这些概念串联起来,介绍遗传算法的执行流程。

  • 在算法初始阶段,它会随机生成一组可行解,也就是第一代染色体。

  • 而后采用适应度函数分别计算每一条染色体的适应程度,并根据适应程度计算每一条染色体在下一次进化中被选中的几率(这个上面已经介绍,这里再也不赘述)。

上面都是准备过程,下面正式进入“进化”过程。

  • 经过“交叉”,生成N-M条染色体;

  • 再对交叉后生成的N-M条染色体进行“变异”操做;

  • 而后使用“复制”的方式生成M条染色体;

到此为止,N条染色体生成完毕!紧接着分别计算N条染色体的适应度和下次被选中的几率。

这就是一次进化的过程,紧接着进行新一轮的进化。

究竟须要进化多少次?

每一次进化都会更优,所以理论上进化的次数越多越好,但在实际应用中每每会在结果精确度和执行效率之间寻找一个平衡点,通常有两种方式。

1. 限定进化次数

在一些实际应用中,能够事先统计出进化的次数。好比,你经过大量实验发现:无论输入的数据如何变化,算法在进化N次以后就可以获得最优解,那么你就能够将进化的次数设成N。

然而,实际状况每每没有那么理想,每每不一样的输入会致使获得最优解时的迭代次数相差甚远,这是你能够考虑采用第二种方式。

2. 限定容许范围

若是算法要达到全局最优解可能要进过不少不少不少次的进化,这极大影响系统的性能。那么咱们就能够在算法的精确度和系统效率之间寻找一个平衡点。咱们能够事先设定一个能够接收的结果范围,当算法进行X次进化后,一旦发现了当前的结果已经在偏差范围以内了,那么就终止算法。

但这种方式也有个缺点,有些状况下可能稍微进化几回就进入了偏差容许范围,但有些状况下须要进化不少不少不少不少次才能进入偏差容许范围。这种不肯定性致使算法的执行效率不可控。

因此,究竟选择何种方式来控制算法的迭代次数,这须要你根据具体的业务场景合理地选择。这里没法给出普世的方式,须要你本身在真实的实践中找到答案。

采用遗传算法解决负载均衡调度问题

算法都是用来解决实际问题的,到此为止,我想你对遗传是算法已经有了个全面的认识,下面咱们就用遗传算法来解决一个实际问题——负载均衡调度问题。

假设有N个任务,须要负载均衡器分配给M个服务器节点去处理。每一个任务的任务长度、每台服务器节点(下面简称“节点”)的处理速度已知,请给出一种任务分配方式,使得全部任务的总处理时间最短。

数学建模

拿到这个问题后,咱们首先须要将这个实际问题映射成遗传算法的数学模型。

任务长度矩阵(简称:任务矩阵)

咱们将全部任务的任务长度用矩阵tasks表示,如:

Tasks={2,4,6,8}

那么,tasks[i]中的i表示任务的编号,而tasks[i]表示任务i的任务长度。

节点处理速度矩阵(简称:节点矩阵)

咱们将全部服务器节点的处理速度用矩阵nodes表示,如:

Nodes={2,1}

那么,nodes[j]中的j表示节点的编号,而nodes[j]表示节点j的处理速度。

任务处理时间矩阵

任务矩阵Tasks节点矩阵Nodes肯定下来以后,那么全部任务分配给全部节点的任务处理时间均可以肯定了,咱们用矩阵timeMatrix表示,它是一个二维数组:

1 2
2 4
3 6
4 8

timeMatrix[i][j]表示将任务i分配给节点j处理所需的时间,它经过以下公式计算:

timeMatrix[i][j] = tasks[i]/nodes[j]

染色体

经过上文咱们知道,每次进化都会产生N条染色体,每一条染色体都是当前问题的一个可行解,可行解由多个元素构成,每一个元素称为染色体的一个基因。下面咱们就用一个染色体矩阵来记录算法每次进化过程当中的可行解。

一条染色体的构成以下:

chromosome={1,2,3,4}

一条染色体就是一个一位数组,一位数组的下标表示任务的编号,数组的值表示节点的编号。那么chromosome[i]=j的含义就是:将任务i分配给了节点j。

上面的例子中,任务集合为Tasks={2,4,6,8},节点集合为Nodes={2,1},那么染色体chromosome={3,2,1,0}的含义是:

  • 将任务0分配给3号节点
  • 将任务1分配给2号节点
  • 将任务2分配给1号节点
  • 将任务3分配给0号节点

适应度矩阵

经过上文可知,在遗传算法中扮演者“上帝”角色的是适应度函数,它会评判每一条染色体的适应度,并保留适应度高的染色体、淘汰适应度差的染色体。那么在算法实现时,咱们须要一个适应度矩阵,记录当前N条染色体的适应度,以下所示:

adaptability={0.6, 2, 3.2, 1.8}

adaptability数组的下标表示染色体的编号,而adaptability[i]则表示编号为i的染色体的适应度。

在负载均衡调度这个实例中,咱们将N个任务执行总时长做为适应度评判的标准。当全部任务分配完后,若是总时长较长,那么适应度就越差;而总时长越短,则适应度越高。

选择几率矩阵

经过上文可知,每次进化过程当中,都须要根据适应度矩阵计算每一条染色体在下一次进化中被选择的几率,这个矩阵以下所示:

selectionProbability={0.1, 0.4, 0.2, 0.3}

矩阵的下标表示染色体的编号,而矩阵中的值表示该染色体对应的选择几率。其计算公式以下:

selectionProbability[i] = adaptability[i] / 适应度之和

遗传算法的实现

上述一切知识点铺垫完成以后,接下来咱们就能够上代码了,相信Talk is cheap, show you the code!

/** * 遗传算法 * @param iteratorNum 迭代次数 * @param chromosomeNum 染色体数量 */
function gaSearch(iteratorNum, chromosomeNum) {
    // 初始化第一代染色体
    var chromosomeMatrix = createGeneration();

    // 迭代繁衍
    for (var itIndex=1; itIndex<iteratorNum; itIndex++) {
        // 计算上一代各条染色体的适应度
        calAdaptability(chromosomeMatrix);

        // 计算天然选择几率
        calSelectionProbability(adaptability);

        // 生成新一代染色体
        chromosomeMatrix = createGeneration(chromosomeMatrix);

    }
}

代码一来,一切都清晰了,彷佛不须要过多的解释了。
上面是遗传算法最主要的框架,其中的一些细节封装在了一个个子函数中。在理解了遗传算法的原理后,我想代码不须要我做过多的解释了吧~完整的代码在个人Github上,欢迎Star。

结果展现

遗传算法结果展现
上述算法一共进行了100次进化,每次进化都会生成100条染色体。图中的横坐标表示进化次数,而纵坐标表示任务执行时间。
从图中咱们能够看到,当进化约20次的时候,算法渐渐收敛于最优解。

写在最后

完整的代码在个人Github上,欢迎下载,欢迎Star!
https://github.com/bz51/GeneticAlgorithm
代码中包含三个文件:

  • ga.html:展现的页面
  • GA.js:遗传算法的完整代码
  • common.js:通用的JS代码

各位大佬直接打开ga.html便可查看算法执行结果。也欢迎各位关注个人我的公众号 “大闲人柴毛毛“,不按期分享不正经程序员的心路历程。