Python实现的遗传算法 及背包问题对最优解的分析


看到CSDN问答区有这么一个问题**“请问如何通过遗传算法得出正确字符串? 40C
比如我希望得到最终结果为"Hello World!"。请结合代码说明,最好是用python。”**
问题链接: link.

代码实现

针对这个问题,自己写了一个。如下:

import random

m=100 #种群数

v=0  #子代中符合进化目标的个体的位置索引

flag=False  #用于标识是否出现目标

target=[72,101,108,108,111,119,32,87,111,114,108,100,33]  #进化目标 list内为Hello World的Ascii码对应数字

l=len(target)

fathers=[[random.randint(32,122) for j in range(l)]for i in range(m)]  #生成父代

sons=[[] for i in range(int(m*(m-1)/2))]   #生成子代list,由于子代为父代两两杂交 所以数量为m*(m-1)/2

def asc(n):  #用于显示某个个体的基因 将ascii码转为字母
    st=""
    tar=[bytes([n[i]]).decode('ascii') for i in range(l)]
    for i in tar:
        st+=i
    return(st)

def zj(fa):  #父代杂交产生子代
    n=0
    for i in range(m-1):
        for j in range(m-i-1):
            sons[n]=fa[i][0:int(l/2)]
            sons[n][int(l/2):l]=fa[j+i+1][int(l/2):l]
            sons[n].append(n)  #末尾添加一位作为评估分数位
            n+=1

def by1():   #子代变异(等位交换)
    for i in sons:
        if (random.randint(0,10)>7):  #有概率变异 
            n=random.randint(0,l-1)
            k=random.randint(0,l-1)
            i[n],i[k]=i[k],i[n]     #基因等位交换

def by2():  #子代变异(突变)
    for i in sons:
        if (random.randint(0,10)>9):  #有概率变异
            i[random.randint(0,l-1)]=random.randint(32,122)

def pg():   #对子代进行评估
    global flag
    global v
    pgs=[]  #存放所有评估分数
    k=0
    for i in sons:
        fs=0
        for j in range(l):
            fs+=abs(i[j]-target[j]) #与目标越接近 分数越接近0
        if fs==0:   #如果评估分为0 则说明出现目标
            v=k
            flag=1
            break
        i[l]=fs
        pgs.append(fs)
        k+=1
    if not flag:
        pgs.sort()  #评估分数由低到高排列
        print("本次进化的最接近评分为%s" %pgs[0])   #本轮当前的最低评分
        n=pgs[m-1]  #获取第m个评估分数
        k=0
        for i in sons:
           if i[l]<=n:     #如果小于等于第m个评估分数则作为新的father
               fathers[k]=i[0:l]
               k+=1
               if k==m:   #当新的father被填满 跳出
                   break

def pg_fathers():   #对父代进行评估
    for i in range(len(fathers)):
        fs=0
        for j in range(len(fathers[0])):
            fs+=abs(fathers[i][j]-target[j]) #与目标越接近 分数越接近0
        if fs==0:
            print("父代的第%d个个体为目标" % i)
            return True
        
    

for i in fathers:
    print(i)
print("=============")
count=0
if not pg_fathers():    #对父代进行评估,如果父代没有目标才进行子代的计算
    while 1:
        zj(fathers)
        by1()
        by2()
        pg()
        count+=1
        if flag:
            break
    print("=====================================")
    print("子代的第%d个个体为进化目标(索引值)" % v)
    print("它的基因为%s" % sons[v][:-1])
    print("它的基因的ASCii码对应的字母为%s" % asc(sons[v]))
    print("一共进化了%d轮" % count)






要注意的是,子代的基因组内的最后一个元素为子代的数组索引值(或评分。取决于运行位置),非基因组成员。

代码运行结果

下面是运行的结果 :
在这里插入图片描述

代码的生物学解释

父代产生子代的机理

在这里插入图片描述
对于有性生殖的生物来说,由父代产生子代基本上是上图的一个过程。
对于任意父代fa[i]来说,它的list内容[x0,x1,x2,…,xn]即为它的基因组。可以把这个list由中间分开,两段分别等价于上图的A1、B1(或A2、B2)。
在方法zj(fa)中,fa[i]可看成上图中的个体1,fa[j+i+1]可看成上图中的个体2。sons[n]=fa[i][0:int(l/2)]表示从个体1中取了一半基因, sons[n][int(l/2):l]=fa[j+i+1][int(l/2):l]表示又从个体2中取了一半基因。此时,子代sons[n]的基因组一半来自父代个体1,一半来自父代个体2,即完成了由父代向子代繁衍的过程。

def zj(fa):  #父代杂交产生子代
    n=0
    for i in range(m-1):
        for j in range(m-i-1):
            sons[n]=fa[i][0:int(l/2)]
            sons[n][int(l/2):l]=fa[j+i+1][int(l/2):l]
            sons[n].append(n)  #末尾添加一位作为评估分数位
            n+=1

子代的变异

等位交换

在这里插入图片描述
生物的基因有的时候会发生不同染色体的等位基因发生交换的情况,称之为等位交换。上图是基因交换的一个过程(并非严格的等位交换)。
我们的遗传算法也需要模拟这个过程,但是如果程序严格按照等位交换来编写对于我们的目的——求最优解——并没有明显可预见的优势。故在编程中实际采用的是随机的两位间的交换,例如下图。在这里插入图片描述
而基因的交换只是有概率发生的,所以在实际程序中加上了 if (random.randint(0,10)>7): ,通过这个判断来使交换的发生概率化。

def by1():   #子代变异(等位交换)
    for i in sons:
        if (random.randint(0,10)>7):  #有概率变异 
            n=random.randint(0,l-1)
            k=random.randint(0,l-1)
            i[n],i[k]=i[k],i[n]     #基因等位交换

突变

在这里插入图片描述
有的时候,生物的基因会发生突变。所谓突变,正如上图所示一般,染色体中的某个基因编码发生了改变。

i[random.randint(0,l-1)]=random.randint(32,122)

上面这段代码正式对这一基因变化过程的一种实现。基因随机的某个位置的编码随机的进行了变化。
当然,这种变化也是有概率的。所以也通过if (random.randint(0,10)>9): 这样的一个判断来进行概率控制。

概率选择

基因的变异概率的取值将直接影响迭代的次数。如果选择的变异概率太小,会导致生物进化的缓慢,正如自然界中产生一个新的物种需要千百万年的时间。为了提高这种变化的概率,在生物科学中常采用的方式之一是辐射育种——通过辐射来提高变异的概率。辐射很可怕,好在对于我们的算法来说,则简单又安全的多——只需要直接更改概率判断语句。
但是,变异的概率并不宜太大。太大的变异概率将导致演化过程极为跳跃——在程序运行的过程中你会发现,有的时候基因评分很快的下降,但是正要接近进化目标时又忽然快速的远离了。

物竞天择

在产生了子代之后,需要对子代进行选择,让更贴近选择压力的个体保留下来。
在这里,我们对子代的适应评分的机制是让子代的各基因位与进化目标基因位进行差运算,然后将差的绝对值累加。绝对值越小,说明越接近进化目标,当绝对值合为零时,说明基因与进化目标完全相符。
然后,按照进化评分对所有子代进行排序,将评分高的作为新的父代继续迭代(相当于只有适合进化方向的个体有交配权,相对较差的个体失去交配权)。

但要注意的是,上面的这种选择方式并非最优的方式。
首先,对于子代的基因评分,如果改用方差的方法来评定显然是更加合理的(可以参见最小二乘法的原理)。
其次,此次的选择是直接让得分低的个体失去产生下一代的权利。但实际的生物演化中,不符合选择压力方向的个体也是有后代的,只是其后代数量的数学期望相较符合选择压力的个体更小。如果将选择改为接近进化目标的个体繁殖概率比其它个体繁殖概率大则更接近实际生物进化情况(但要注意的是,接近实际情况并不代表对于我们的求解是更好的选择,其需要迭代的次数往往更多)。

背包问题举例说明最优解和近似最优解

  利用遗传算法,我们可以对背包问题等一系列求解最佳值的问题进行求解。但要注意的是,由于进化并不总是超最好的方向进行——因为基因突变有着随机性,很可能一个本来很接近最佳值的基因在下一轮的突变中变得远离了最优解。

  
在这里插入图片描述
  上图绘制了通过遗传算法求解背包问题每一代所产生的背包最佳值和平均值。其中,蓝色曲线是每一代进化里的最佳值,橙色曲线是每一代进化里的平均背包价值。我们可以发现,无论是最优解还是平均的背包重量,都是有所起伏的。这是正是因为上文所说的因为基因突变有着随机性,很可能一个本来很接近最佳值的基因在下一轮的突变中变得远离了最优解。
  为了改进这个问题,我们可以设置基因突变的范围只在子代中进行。如此一来,上一代的最优解不会被突变干扰,从而得到保留。这样一来,进化的最优解要么向更好的方向进行,要么保持现有的最优解。在进行了这样的更改后,进化的曲线如下图。我们可以看到进化的效率更高了。
在这里插入图片描述
  
  上图所示的进化进行了40轮。下图则是进行了400轮进化的结果。我们可以明显看出,进化到了100轮左右进化曲线才进入水平阶段(即接近或达到最优解)。显然,在进化轮数有线的情况下,我们更多的时候是得到一个接近最优解的近似最优解——例如在下图中我们可以发现,40轮进化的时候,所得到的最佳值已经是100轮进化时的最佳值的90%。
  在实际运用中,由于我们不知道到底多少论进化才能达到最优解。有些时候甚至因为时间的限制而只能进行十分有线的迭代。所以一般来讲,使用遗传算法获得的最优解只是近似最优解。但是一般来说在工程中,绝大多数的问题都不可能获得最优解,并且近似解往往已经足够好,所以遗传算法在实际使用中仍然是一个非常好的选择。
在这里插入图片描述

示例程序

遗传算法解背包问题的python程序

  • 2
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

soar3033

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值