遗传算法详解 附python代码实现

遗传算法

看了好久才把遗传算法搞懂,附一个链接这个是我看过有关遗传算法讲解最详细的一篇https://blog.csdn.net/ha_ha_ha233/article/details/91364937

什么是遗传算法

遗传算法是用于解决最优化问题的一种搜索算法。从名字来看,遗传算法借用了生物学里达尔文的进化理论:“适者生存,不适者淘汰”,将该理论以算法的形式表现出来就是遗传算法的过程。

主要过程

  1. 初始化一个种群,种群中的个体DNA表示
  2. 种群中的个体进行交叉变异产生后代
  3. 根据后代中每个个体适应度进行自然选择、优胜劣汰
  4. 不断迭代产生最优种群

例子

以求解二元函数为例,体会遗传算法如何解决最优化问题

def F(x,y):
    return 3*(1-x)**2*np.exp(-(x**2)-(y+1)**2)- 10*(x/5 - x**3 - y**5)*np.exp(-x**2-y**2)- 1/3**np.exp(-(x+1)**2 - y**2)

在x ∈ [ − 3 , 3 ] , y ∈ [ − 3 , 3 ] 范围里求函数的最大值问题

种群和个体的概念

遗传算法启发自进化理论,而进化是以种群为单位的,而种群是一定空间范围内同时生活着的同种生物的全部个体,显然种群与个体是遗传算法里必不可少的概念;在遗传算法里,个体通常为某个问题的一个解,并且该解在计算机中被编码为一个向量表示! 例如,求二元函数F(x, y)的最大值,该问题的解即为一组 (x,y) 取值,如(x=1.3,y=3.5)、(x=2.6,y=3.7)…这里的每一个(x,y)对即为一个个体,而一组一组可能解的集合就叫种群,比如在这个问题中设置100个这样的x,y的可能的取值对,这100个个体就构成了种群

编码、解码与DNA

在上述提到的个体(一组可能解)在程序中被表示成一组向量,将实数表示成向量的过程称为编码 ,于是问题就转化成了将两个实数表示成一组向量的问题(这里转化成向量表示是为了之后的交叉和变异),这里可以类比生物的DNA,生物DNA有四种碱基对–ACGT 通过不同的组合构成不同的个体,而计算机只有0和1两种 通过不同的0和1组成的串构成不同的个体(例子中的函数解),而这个0和1组成的串可以映射成不同的十进制数
编码映射

y=f(x) x是实数,y是二进制数

这个映射可以不用关心,因为计算机本身操纵的就是二进制数,二进制串也可以随机产生

# pop表示种群矩阵,一行代表一个个体(DNA),POP_SIZE为矩阵行数(个体个数),DNA_SIZE为编码长度
  pop=np.random.randint(2,size=(POP_SIZE,DNA_SIZE*2))

没有必要将一个十进制数转化为一个二进制数,但是后面肯定要将编码的二进制转换成我们理解的十进制映射相应的实数值代入公式求函数值,因此必然要用到y=f(x)的逆映射,也就是将二进制转化为十进制,这个过程叫做解码!!!

首先我们限制二进制串的长度为10(长度自己指定即可,越长精度越高),例如我们有一个二进制串(在程序中用数组存储即可)

[0,1,0,1,1,1,0,1,0,1]

这个二进制串如何转化为实数呢?不要忘记我们的x , y ∈ [ − 3 , 3 ] 这个限制,我们目标是求一个逆映射将这个二进制串映射到x , y ∈ [ − 3 , 3 ] 即可,为了更一般化我们将x , y 的取值范围用一个变量表示,在程序中可以用python语言写到:

X_BOUND=[-3,3]  # x的取值范围
Y_BOUND=[-3,3]  # y的取值范围

为将二进制串映射到指定范围,首先先将二进制串按权展开,将二进制数转化为十进制数,我们有
0 * 29 + 1 * 28 + 0 * 27 + 1 * 26 + 1 * 25 + 1 * 24+ 0 * 23 + 1 * 22 + 0 * 21 + 1 * 20 = 373,将转换后的实数压缩到[ 0 , 1 ]之间的一个小数,373/(2 10−1)≈0.36461388074,通过以上这些步骤所有二进制串表示都可以转换为[ 0 , 1 ]之间的小数,现在只需要将[ 0 , 1 ] 区间内的数映射到我们要的区间即可。假设区间[ 0 , 1 ]内的数称为num,转换在python语言中可以写成:

#X_BOUND,Y_BOUND是x,y的取值范围 X_BOUND = [-3, 3], Y_BOUND = [-3, 3],
x_ = num * (X_BOUND[1] - X_BOUND[0]) + X_BOUND[0] #映射为x范围内的数
y_ = num * (Y_BOUND[1] - Y_BOUND[0]) + Y_BOUND[0] #映射为y范围内的数

通过上述步骤完成解码任务
解码过程python代码:

def translateDNA(pop):
    '''
    解码
    :param pop: 种群矩阵,一行表示一个二进制编码的个体(可能解),行数为种群中个体数目
    :return: 返回的x,y 是一个行 为种群大小 列为 1 的矩阵 每一个值代表[-3,3]上x,y的可能取值(十进制数)
    '''
    x_pop=pop[:,1::2]  # pop中的奇数列表示x
    y_pop=pop[:,0::2]  # pop中的偶数列表示y
    x=x_pop.dot(2**np.arange(DNA_SIZE)[::-1])/float(2**DNA_SIZE-1)*(X_BOUND[1]-X_BOUND[0])+X_BOUND[0] 
    y=y_pop.dot(2**np.arange(DNA_SIZE)[::-1])/float(2**DNA_SIZE-1)*(Y_BOUND[1]-Y_BOUND[0])+Y_BOUND[0]
    return x,y

适应度和选择

我们已经得到了一个种群,现在要根据适者生存规则把优秀的个体保存下来,同时淘汰掉那些不适应环境的个体。现在摆在我们面前的问题是如何评价一个个体对环境的适应度?在我们的求最大值的问题中可以直接用可能解(个体)对应的函数的函数值的大小来评估,这样可能解对应的函数值越大越有可能被保留下来,以求解上面定义的函数F的最大值为例,

# 得出每个个体的适应度
def get_fitness(pop):
    x,y=translateDNA(pop)
    pred=F(x,y)  
    return (pred-np.min(pred))+1e-3
def select(pop,fitness):   # 自然选择,优胜劣汰   
    idx=np.random.choice(np.arange(POP_SIZE),size=POP_SIZE,replace=True,p=(fitness)/fitness.sum())
    return pop[idx]

pred是将可能解带入函数F中得到的预测值,得到的也是一个矩阵,因为后面的选择过程需要根据个体适应度确定每个个体被保留下来的概率,而概率不能是负值,所以减去预测中的最小值把适应度值的最小区间提升到从0开始,但是如果适应度为0,其对应的概率也为0,表示该个体不可能在选择中保留下来,这不符合算法思想,遗传算法不绝对否定谁也不绝对肯定谁,所以最后加上了一个很小的正数。select()函数中主要是使用了choice里的最后一个参数p,参数p描述了从np.arange(POP_SIZE)里选择每一个元素的概率,概率越高约有可能被选中,最后返回被选中的个体即可。

交叉、变异

通过选择我们得到了当前看来“还不错的基因”,但是这并不是最好的基因,我们需要通过繁殖后代(包含有交叉+变异过程)来产生比当前更好的基因,但是繁殖后代并不能保证每个后代个体的基因都比上一代优秀,这时需要继续通过选择过程来让试应环境的个体保留下来,从而完成进化,不断迭代上面这个过程种群中的个体就会一步一步地进化。

具体地繁殖后代过程包括交叉和变异两步。交叉是指每一个个体是由父亲和母亲两个个体繁殖产生,子代个体的DNA(二进制串)获得了一半父亲的DNA,一半母亲的DNA,但是这里的一半并不是真正的一半,这个位置叫做交配点,是随机产生的,可以是染色体的任意位置。通过交叉子代获得了一半来自父亲一半来自母亲的DNA,但是子代自身可能发生变异,使得其DNA即不来自父亲,也不来自母亲,在某个位置上发生随机改变,通常就是改变DNA的一个二进制位(0变到1,或者1变到0)。

需要说明的是交叉和变异不是必然发生,而是有一定概率发生。先考虑交叉,最坏情况,交叉产生的子代的DNA都比父代要差(这样算法有可能朝着优化的反方向进行,不收敛),如果交叉是有一定概率不发生,那么就能保证子代有一部分基因和当前这一代基因水平一样;而变异本质上是让算法跳出局部最优解,如果变异时常发生,或发生概率太大,那么算法到了最优解时还会不稳定。交叉概率,范围一般是0.6~1,突变常数(又称为变异概率),通常是0.1或者更小。

# 交叉、变异
def crossover_and_mutation(pop,CROSSVER_RATE=0.8):
    new_pop=[]
    for father in pop:    # 遍历种群中的每一个个体,将该个体作为父亲
        child=father      # 孩子先得到父亲的全部基因(代表一个个体的一个二进制0,1串)
        if np.random.rand() <CROSSVER_RATE:  #产生子代时不是必然发生交叉,而是以一定的概率发生交叉
            mother=pop[np.random.randint(POP_SIZE)]  # 在种群中选择另一个个体作为母亲
            cross_points=np.random.randint(low=0,high=DNA_SIZE*2)  #随机产生交叉的点
            child[cross_points:]=mother[cross_points:]
        mutation(child)
        new_pop.append(child)
    return new_pop

def mutation(child,MUTATION_RATE=0.1):
    if np.random.rand()<MUTATION_RATE:
        mutate_points=np.random.randint(0,DNA_SIZE*2)  # 随机产生一个实数,代表要变异基因的位置
        child[mutate_points]=child[mutate_points]^1    # 将变异点位置的二进制反转

完整代码

# 遗传算法
import numpy as np

DNA_SIZE=8
POP_SIZE=200
CROSSVER_RATE=0.9
MUTATION_RATE=0.01
N_GENERATIONS=5000
X_BOUND=[-3,3]  # x的取值范围
Y_BOUND=[-3,3]  # y的取值范围

def F(x,y):
    return 3*(1-x)**2*np.exp(-(x**2)-(y+1)**2)- 10*(x/5 - x**3 - y**5)*np.exp(-x**2-y**2)- 1/3**np.exp(-(x+1)**2 - y**2)

# 得到最大适应度
def get_fitness(pop):
    x,y=translateDNA(pop)
    pred=F(x,y)
    return (pred-np.min(pred))+1e-3

def translateDNA(pop):
    '''
    解码
    :param pop: 种群矩阵,一行表示一个二进制编码的个体(可能解),行数为种群中个体数目
    :return: 返回的x,y 是一个行 为种群大小 列为 1 的矩阵 每一个值代表[-3,3]上x,y的可能取值(十进制数)
    '''
    x_pop=pop[:,1::2]  # pop中的奇数列表示x
    y_pop=pop[:,0::2]  # pop中的偶数列表示y
    x=x_pop.dot(2**np.arange(DNA_SIZE)[::-1])/float(2**DNA_SIZE-1)*(X_BOUND[1]-X_BOUND[0])+X_BOUND[0]
    y=y_pop.dot(2**np.arange(DNA_SIZE)[::-1])/float(2**DNA_SIZE-1)*(Y_BOUND[1]-Y_BOUND[0])+Y_BOUND[0]
    return x,y

# population_matrix = np.random.randint(2, size=(POP_SIZE, DNA_SIZE * 2))
# print(len(translateDNA(population_matrix)[0]))

# 交叉、变异
def crossover_and_mutation(pop,CROSSVER_RATE=0.8):
    new_pop=[]
    for father in pop:    # 遍历种群中的每一个个体,将该个体作为父亲
        child=father      # 孩子先得到父亲的全部基因(代表一个个体的一个二进制0,1串)
        if np.random.rand() <CROSSVER_RATE:  #产生子代时不是必然发生交叉,而是以一定的概率发生交叉
            mother=pop[np.random.randint(POP_SIZE)]  # 在种群中选择另一个个体作为母亲
            cross_points=np.random.randint(low=0,high=DNA_SIZE*2)  #随机产生交叉的点
            child[cross_points:]=mother[cross_points:]
        mutation(child)
        new_pop.append(child)
    return new_pop

def mutation(child,MUTATION_RATE=0.1):
    if np.random.rand()<MUTATION_RATE:
        mutate_points=np.random.randint(0,DNA_SIZE*2)  # 随机产生一个实数,代表要变异基因的位置
        child[mutate_points]=child[mutate_points]^1    # 将变异点位置的二进制反转

def select(pop,fitness):   # 自然选择,优胜劣汰
    idx=np.random.choice(np.arange(POP_SIZE),size=POP_SIZE,replace=True,p=(fitness)/fitness.sum())
    return pop[idx]

def print_info(pop):
    fitness=get_fitness(pop)
    max_fitness_index=np.argmax(fitness)
    # print('此时种群',pop)
    # print('max_fitness:',fitness[max_fitness_index])
    x,y=translateDNA(pop)
    # print('最优基因型:',pop[max_fitness_index])
    # print('(x,y):',x[max_fitness_index],y[max_fitness_index])
    print('max_fitness:%s,函数最大值:%s'%(fitness[max_fitness_index],F(x[max_fitness_index],y[max_fitness_index])))

if __name__=='__main__':
    pop=np.random.randint(2,size=(POP_SIZE,DNA_SIZE*2))
    for i in range(N_GENERATIONS):
        x,y=translateDNA(pop)
        pop=np.array(crossover_and_mutation(pop))  # 交叉变异
        fitness=get_fitness(pop)  # 得到适应度
        pop=select(pop,fitness)   # 优胜劣汰
        # if(i%100==0):
        #     print('第%s次迭代:'%i)
        #     print_info(pop)
    print_info(pop)

最后,这种算法得到的种群是相对与之前的比较优秀,但对应的最大值却不一定比之前淘汰的种群对应的函数最大值要大,现在的种群只是相对于之前的更收敛罢了,附一张运行截图
在这里插入图片描述

THE END
分享
二维码
< <上一篇
下一篇>>