遗传算法(1)

时间:2022-05-08
本文章向大家介绍遗传算法(1),主要内容包括1 进化过程、2 算法过程、3 背包问题、2. 设计初始群体、3. 适应度计算、4. 生产下一代、5. 迭代计算、6. 注意事项、基本概念、基础应用、原理机制和需要注意的事项等,并结合实例形式分析了其使用技巧,希望通过本文能帮助到大家理解应用这部分内容。

与其说遗传算法是一个算法,不如说是一种处理问题的思想方式更为恰当,因为遗传算法整个体系说来说去都是在说对于一种问题处理的思路和原则,而不是一个具体的代码编写过程。

遗传算法(Genetic Algorithm)是一类借鉴生物界的进化规律(适者生存,优胜劣汰遗传机制)演化而来的随机化搜索方法。它是由美国的J.Holland教授1975年首先提出的,不过它借鉴的可是进化论的理论依据。在这个体系里,思维方式远比编写代码重要,所以我们先用一点时间来缅怀一下遗传算法真正的鼻祖,著名的英国生物学家查尔斯·罗伯特·达尔文——进化论的奠基人。

进化论不是一本书,而是一个有关物种发源与发展的逻辑体系。达尔文在1859年出版了《物种起源》,由此开创的“物竞天择,适者生存”的进化论体系被科学界公认为19世纪自然科学的三大发现之一(另外两个是细胞学说和能量守恒与转化定律)。达尔文于1882年4月病逝,为了表示对这位科学家的崇敬,大家把他安葬在牛顿墓的旁边,位于英国伦敦的威斯敏斯特大教堂——也就是我们平时称的西敏寺。

1 进化过程

言归正传,我们说说这类算法的思路是怎么完成的。我们想想在物种进化中的例子,最开始地球上是只有海洋没有陆地的,从单细胞动物到鱼不知道进化了多少代。我们就说从陆地出现以后,第一批从海里想上岸去谋生的鱼吧,虽然谋生的具体动机可能不太明确。

估计是一开始有一群鱼,他们由于种种原因跟别的鱼有了差别,比如胆儿比较肥——起码敢上岸去玩,也有个能直接呼吸空气的鳃——起码能在空气里还能存活,还有也需要有格外强壮的鳍——能跑得快一些。这是最起码的吧,不然也上不了岸。再后来这些鱼里就有技能更邪门一些的,一些在陆上格外倚重的器官如果变异强大了就会继续支持他们在陆上生活,比如粗壮的鳍,空气摄入能力更强的肺,更好的眼神等等。这算是生活所迫吧,反之要么就死翘翘,要么就还是乖乖溜回到水里去生活,适应的种群才会在相应的环境里生存下来。

根据进化论的观点“物竞天择,适者生存”,生物自己是会进行一代一代变化的。这个变化本来是没有什么方向的,生物自己也控制不了。变化由什么而来?

第一,父母的基因进行交换重组;

第二,基因突变。

一代一代在整个种群的不同个体力有无数次重组的机会,也有一定的基因突变的机会,导致了若干代以后,同一个种群之间的样态可能会非常不一样。咱们就瞅瞅现在的人就知道了,人有23对染色体,每一代的重组和突变使得人的多样性特点非常明显。现在人长相彼此有很大差别,还有体态、性格、思维方式、疾病抵御……这些方面也都是千差万别。

尤其是人类历史上经历过若干次大的瘟疫,都是杀人盈野杀人盈城的恶魔——14世纪欧洲的黑死病,造成全世界超过7000万人死亡;17世纪在欧洲肆虐的天花病毒也杀死超过4000万的欧洲人。14世纪得了黑死病那基本就是没救(是一种鼠疫杆菌),防治天花的牛痘疫苗也是到了18世纪才研发出来,能够存活下来的人群其实也并没有接受什么像样治疗,科学家分析基本只能解释为基因层面对抗的胜利——这些存活的人的基因比那些患病死去的人有着对这种疾病对抗更强有力的成分。

不论是由于基因自身发生的突变,还是由于组合产生的新特性,这些都是不确定性的变化。而客观世界上会有很多变化对人类种群做出这种选择的裁剪,出现疾病就是裁剪那些对疾病耐受力弱的人,出现饥荒就是裁剪那些对饥饿耐受力弱的人,出现严寒就是裁剪那些对严寒耐受力弱的人。哪怕不耐寒基因里还夹杂着长得帅的基因,那估计也同样不好使,很残酷的地方就在这里,没有什么对错,只有适应不适应。这种“进化”可以说谈不到“进化”,而是被动的演化而后被动地被选择。

2 算法过程

在了解了进化基因层面的过程后,是要落实到算法过程上去的。那么我们怎么来构建这个算法过程呢?

其实关键抓住几步就可以。

(1)基因编码

在这个过程中,我们尝试着对一些个体的基因做一个描述,构造这些基因的结构,有点像确定函数自变量的过程。

(2)设计初始群体

在这个环节,我们需要在这个我们自己造出来的小世界里冒充一把“上帝”(基督徒读者们请原谅我的冒犯),要造一个种群出来,这些种群有很多生物个体但是基因都不相同。

(3)适应度计算(剪枝)

在这个环节,我们要造一些“上帝的剪刀”对那些不太适应的个体进行裁剪,不让他们产生后代。这不是讲人道主义的时候,和我们最终遴选规则差异大的个体肯定不适合作为备选对象,该减掉一定要减掉,否则它产生的后代只会让计算量更大而距离逼近目标没有增益。

(4)产生下一代

产生下一代这个部分有三种办法:直接选择,基因重组,基因突变。

而后再回到3进行循环,适应度计算,产生下一代,这样一代一代找下去,直到找到最优解为止。

遗传算法在解决很多领域的问题时都体现出很好的特性,例如:

TSP问题(Traveling Salesman Problem即旅行商问题也叫货郎担问题)、九宫问题(八数码问题)、生产调度问题(Job Shop Scheduling)、背包问题((Knapsack Problem即NP问题)等。

流程是这样很简单了,不过要看一个具体的解题实例才能帮我们更好地理解。

3 背包问题

背包问题是一种组合优化的NP(即多项式复杂程度的非确定性问题)完全问题,这类NP问题的虽然听起来很拗口但是特点很明显,那就是“生成问题的一个解通常比验证一个给定的解时间花费要多得多”。

如果还是觉得不好理解我们就先来看一个例子。

第一个例子是合数分解质因数问题,如果现在有一个命题,请分解合数698975355227为几个质数相乘的结果。那么我们怎么算?只能是把质数从2穷举到根号下698975355227

,2个质数一起、3个质数一起、4个质数一起……尝试着做各种组合相乘看是否等于这个结果,这是一个非常耗时的工作,即便我们有计算机,这个过程也非常耗时。但是我们如果反过来说,698975355227是否是809、887、977、997这四个素数的乘积,那么验证起来会非常快,只要我们手边有一个普通的计算器,半分钟都要不了就能验算出来——确实如此。这就是所谓的“生成问题的一个解通常比验证一个给定的解时间花费要多得多”。

我们在很多资料上都能找到背包问题的描述,它的大意是这样:

有N件物品和一个容量为V的背包。第i件物品的重量是w[i],价值是v[i]。求解将哪些物品装入背包可使这些物品的重量总和不超过背包容量,且价值总和最大。

这种问题就是典型的NP问题,验证一个我们猜想的解,比我们算出一组解要快很多很多。

我们来具体看一些数字可能会更直观一些。

假设有一个背包,可以放置80公斤的物品。此外,还有6件物品:

怎么放置才能让背包里的物品价值总和尽可能多呢?

有几种思路,第一种就是穷举法。每种物品只有存在(1)和不存在(0)两种状态,那么一共可能产生的方案最多是26个也就是64个。把每种组合具体的重量和价值都算出来,超过80公斤的方案就直接pass掉,少于80公斤的就留下并且记一下货品总价值,到最后比较一下每种方案的总价值大小就可以了。这是一种可行的方法,没问题。

不过如果有128种物品这个方案还可行吗?如果穷举的话就是大约3.4×1038种方案,如果计算机一秒能够验证一亿种情况,大概需要1.08×1023年才能算出来,这恐怕实在行不通。

这种情况下,遗传算法就显示出优势来了。我们看看在6个物品的情况里怎么来求解。

Step 1. 基因编码

一共6种物品,每种物品的有无都可以作为独立的一个基因片段:

一共是6位。

那么假如只有物品2,物品3和物品6的情况,染色体是:011001

2. 设计初始群体

为了计算方便我们设置初始群体为4个初始生物个体好了,随机产生。

100100,对应物品1,物品4存在;

101010,对应物品1,物品3,物品5存在;

010101,对应物品2,物品4,物品6存在;

101011,对应物品1,物品3,物品5,物品6存在。

3. 适应度计算

适应度计算首先是要有一个适应度的函数来做标尺。

我们设计适应度的函数为物品总价值,那么4个个体各自适应度结果是多少呢:

基因100100=15+45=60,

基因101010=15+35+55=105,

基因010101=25+45+70=140,

基因101011=15+35+55+70=175。

在这里不要忘了一点,就是物品本身还有重量,所以重量也要作为判断标准之一,各自的重量分别是多少呢:

基因100100=10+25=30,

基因101010=10+20+30=60,

基因010101=15+25+35=75,

基因101011=10+20+30+35=95。

基因101011基因中的这一种显然是已经超过了我们要求的80公斤而直接被淘汰,我们直接理解为人的进化中有一种不知出于何种原因直接体重飙升到500公斤,然后?然后就没有然后了,智商再高也不好使了。

这样还剩下:

基因100100,适应函数60;

基因101010,适应函数105;

基因010101,适应函数140。

除了刚才有一个体重飙升太严重自爆的个体以外,这些没自爆的个体这时候有一个遴选的过程,那就是有一些更“健壮的”将有更多的机会生存下去。

我们先把适应函数求和,60+105+140=305。下面进行一个用类似轮盘赌来进行遴选的过程:

赌博我们肯定是不提倡的,这里只是用来做一个形象的比喻。轮盘赌是一种赌博游戏,就像上图所示的样子,整个游戏道具是一个大木盘,可以进行旋转。木盘上刻着很多小格子,每个小格子上有数字,在轮盘开始旋转之后,放入一个小球沿盘面滚动与轮盘旋转方向相反。待轮盘静止后,小球掉入的各自所对应的标号即为获胜号码。从古典概型来看,每个格子的胜率应该是一样的。

现在我们想象一下,这个轮盘上有305个格子,

其中基因100100作为一个玩家选取了其中的60个小格子,

基因101010作为一个玩家选用了105个小格子,

基因010101作为一个玩家选择了140个小格子,

分别作为自己押注的赌点。

转动4次——这个推荐为每一代个体的数量(在这个小世界里,您是上帝您决定,我们这个例子里是4)。那么每旋转一次,“中奖”的这个基因组就允许繁殖一次,如果一次都没中奖那真的是太不幸了,这个基因将无法得到延续。在这个例子里,我们看到

基因100100每次被遴选的概率为60/305,

基因101010每次被遴选的概率为105/305,

基因101010每次被遴选的概率为140/305。

在我计算的结果里,基因101010和基因010101各繁殖两次,在你自己模拟计算的过程中可能又不是这个结果,没关系,我们这里只说大体的思路。

4. 生产下一代

基因101010和基因010101在成功被遴选后,需要进行基因重组来产生下一代。计算过程如下表所示。

两个被遴选后的基因进行了基因重组,其中一对从第三位后面断开,尾部进行了交换,另一对从第四位后面断开,尾部进行了交换。这样又产生了4个不同的基因。一般来说交叉点位置是可以随机选取的。如图所示,两段不同的基因从中间断开后进行结合,上段的前半部和下段的后半部结合成为新的基因,而下段的前半部和上段的后半部结合成为新的基因。

在基因重组之后是可以有一个基因突变的过程的,就是随机把一定比例的基因里的某一位或者某几位做变化——1变成0,0变成1。这个过程建议还是取法一般的生物繁殖过程,让变异的基因比率低一些比较好,在这个例子里我们没有做变异。

5. 迭代计算

下面就是一代一代用这种准则做下去了,我们直接求重量和价值了:

基因101101,重量90,价值(适应函数)165;(直接淘汰)

基因010010,重量45,价值(适应函数)80;

基因101001,重量65,价值(适应函数)120;

基因010110,重量70,价值(适应函数)125。

在这里我们看到一个现象,总体的适应函数和为80+120+125=325,比第上一代的60+105+140=305要普遍适应性更好,貌似是进化了,但是上一代是有一个适应函数140的“超强基因个体”的,这一代却没有一个能够超过去。

在一次完整的计算中,迭代过程可能会经历几十代甚至更久,如果发现出现了连续几代适应函数基本不增加或者甚至反而减少的情况,那就说明函数已经收敛了。

“收敛”这个词如果没有在算法学习中接触过,那么来说一个形象点的例子,咱们就想象我们平时在体重秤上称量的时候。当人站上去的时候,指针就开始抖动,抖动幅度越来越小,最后基本稳定在一个值。稳定后,我们就读取这个数字就好了。假设体重秤称量是有算法控制的,那么这个摆动几下很快就能稳定在一个值的就是收敛性比较快(比较好)的算法;要摆动很久很久才能稳定的就是收敛性比较慢(比较差)的算法;如果摆幅随着时间的推移反而越来越大,那收敛性就非常不好,通常就没有解。这种类比的理解更形象一些。

像我们刚刚这个例子里,可以就此结束迭代操作,也可以再观察一代的到两代的变化,都是可以的。收敛的速度会因很多因素而变化,比如基因位的长度,基因重组时的方案,基因变异的程度,每一代产生个体的数量等。一般发生适应函数收敛的时候就是迭代结束的时候。而我们在迭代结束前找到的最优的解就是我们要的解。

6. 注意事项

在使用遗传算法的时候请注意几个地方,这几个地方是可以进行调整的:

(1)初始群体

初始群体的数量是可以调整的,我们可以想想,在刚刚的6个物品的背包问题的极限是我直接生成所有的情况,26也就是64个个体全部列出作为初始群体。但是这毫无意义,也不是我们要使用基因算法的目的。我的看法是,或许可以考虑初始群体的数量可以设置为N个,N为当前计算机最大可并行计算的数量,比如是8核心的计算机,那就可以设置为8个个体作为初始群体。在每次产生基因后把不同的计算放到不同的线程中去。当然,这个也要视并行对算法效率的改善程度而定。此外,就是定性考虑一下,初始数量太少可能会导致在向量空间中覆盖面积过小而导致收敛到了非最优解就终止了算法。

(2)适应度函数

适应度函数中的轮盘赌算法只是其中之一,也可以考虑使用别的算法进行遴选。注意我们的遴选原则,其实是从生物多样化中进行挑选的一个思路。所以淘汰比较弱的基因是可以的,但是不建议淘汰的比例太大。

(3)基因重组

基因重组这个环节是变数比较大的。断开的位置几乎是可以随意进行的,比如我们刚刚看到的这个例子,一个6位长度的基因,1-5断开,2-4断开,3-3断开,4-2断开,5-1断开,都是可以选的方案。其实在一次产生后代的过程中是可以允许以不同的方案产生多个后代的,比如两个配对的基因是可以用2-4方案做两个后代,同时再用4-2方案做两个后代的,4个后代,这是可以的。这样会带来更大的基因丰富性,不过同时也要注意计算量如果发生增长,在若干代以后恐怕会严重影响计算性能。

另外相信大家也能意识到,不要一个基因自身和自身去做重组,没有意义,因为怎么重组还是自己没有任何变化——近亲结婚害处大,至少是不会有试出更新更好方案的可能。

(4)迭代结束

这个算法迭代结束的判断标准因人而异,但是总体来说的原则就是刚刚说过的,如果连续几代观察都没有明显的适应函数的增长,那就说明进化到这几代基本“到头”了。在结束迭代的时候,我们查查看在这之前找到的最优解就是我们能找到的最优解。

对于刚刚背包问题的解,在这里我们也同样给出一段python代码请大家参考:

# coding=utf-8

import random

#背包问题

# 物品重量价格

X = {

1: [10,15],

2: [15,25],

3: [20,35],

4: [25,45],

5: [30,55],

6: [35,70]}

#终止界限

FINISHED_LIMIT = 5

#重量界限

WEIGHT_LIMIT = 80

#染色体长度

CHROMOSOME_SIZE = 6

#遴选次数

SELECT_NUMBER = 4

max_last = 0

diff_last = 10000

#收敛条件、判断退出

def is_finished(fitnesses):
    globalmax_last
    globaldiff_last
   max_current = 0
    for v infitnesses:
        ifv[1] > max_current:
           max_current = v[1]
    diff =max_current - max_last
    if diff< FINISHED_LIMIT and diff_last < FINISHED_LIMIT:
       return True
    else:
       diff_last = diff
       max_last = max_current
       return False

#初始染色体样态

def init():
   chromosome_state1 = '100100'
   chromosome_state2 = '101010'
   chromosome_state3 = '010101'
   chromosome_state4 = '101011'
   chromosome_states = [chromosome_state1,
                        chromosome_state2,
                        chromosome_state3,
                        chromosome_state4]
    returnchromosome_states

#计算适应度

def fitness(chromosome_states):
    fitnesses= []
    forchromosome_state in chromosome_states:
       value_sum = 0
       weight_sum = 0
        fori, v in enumerate(chromosome_state):
           if int(v) == 1:
               weight_sum += X[i + 1][0]
               value_sum += X[i + 1][1]
       fitnesses.append([value_sum, weight_sum])
    returnfitnesses

#筛选

def filter(chromosome_states, fitnesses):
    #重量大于80的被淘汰
    index =len(fitnesses) - 1
    whileindex >= 0:
        index-= 1
        iffitnesses[index][1] > WEIGHT_LIMIT:
           chromosome_states.pop(index)
           fitnesses.pop(index)

#遴选

   selected_index = [0] * len(chromosome_states)
    for i inrange(SELECT_NUMBER):
        j =chromosome_states.index(random.choice(chromosome_states))
       selected_index[j] += 1
    returnselected_index

#产生下一代

def crossover(chromosome_states, selected_index):
   chromosome_states_new = []
    index =len(chromosome_states) - 1
    whileindex >= 0:
        index-= 1
       chromosome_state = chromosome_states.pop(index)
        for iin range(selected_index[index]):
           chromosome_state_x = random.choice(chromosome_states)
           pos = random.choice(range(1, CHROMOSOME_SIZE - 1))
           chromosome_states_new.append(chromosome_state[:pos] +chromosome_state_x[pos:])
       chromosome_states.insert(index, chromosome_state)
    returnchromosome_states_new
if __name__ == '__main__':

#初始群体

   chromosome_states = init()
    n = 100
    while n> 0:
        n -=1

#适应度计算

       fitnesses = fitness(chromosome_states)
        ifis_finished(fitnesses):
            break

#遴选

       selected_index = filter(chromosome_states, fitnesses)

#产生下一代

       chromosome_states = crossover(chromosome_states, selected_index)

# 1: [[60, 35], [105, 60], [140, 75], [175, 95]]

# 2: [[60, 35], [105, 60], [80, 45], [90, 50]]

# 3: [[95, 55], [115, 65], [70, 40], [90, 50]]

# 4: [[70, 40], [70, 40], [150, 85], [115, 65]]

# 5: [[115, 65], [115, 65], [115, 65], [70, 40]]

# ['100110', '100110', '100110', '100110']

我们求出的[115, 65]这个就是要求的解,对应的货物是1:[10, 15]、4:[25, 45]、5:[30, 55]这三件货物。

这里我们用的收敛条件是连续两代的适应函数最大值都不再增加,这种情况判断为收敛。