1、遗传算法简介
遗传算法(Genetic Algorithm,GA)最早是由美国的 John holland于20世纪70年代提出,该算法是用于解决最优化问题的一种搜索算法。它是模拟达尔文生物进化论的自然选择和遗传学机理的生物进化过程的计算模型,通过数学的方式,利用计算机仿真运算,将问题的求解过程转换成类似生物进化中的染色体基因的交叉、变异等过程。其本质是一种高效、并行、全局搜索的方法,能在搜索过程中自动获取和积累有关搜索空间的知识,并自适应地控制搜索过程以求得最佳解。
2、问题引入
遗传算法是用来解决最优化问题的,下面以求一个二元函数在 x∈[−3,3],y∈[−3,3] 范围里的最大值为例子来详细讲解遗传算法的每一步。选取的二元函数为:
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)
将函数可视化,可视化代码和运行结果如下:
%matplotlib notebook # 为使结果图能旋转,本次使用的编辑器为jupyter notebookimport numpy as np import matplotlib.pyplot as pltplt.rcParams['font.sans-serif']=['SimHei']plt.rcParams["axes.unicode_minus"] = Falsedef 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 = np.arange(-3, 3, 0.01) # 可视化的 x 坐标范围y = np.arange(-3, 3, 0.01) # 可视化的 y 坐标范围# 生成 x-y 平面采样网格点,方便可视化X,Y = np.meshgrid(x,y)Z = F(X,Y) # 计算网格点上的函数值ax = plt.axes(projection='3d')ax.plot_surface(X,Y,Z,cmap='rainbow') # 3D 曲面图ax.view_init(60, -30)ax.set_xlabel('x')ax.set_ylabel('y')plt.show()
可以很容易发现函数在这个局部的最大值大概在当x ≈ 0 , y ≈ 1.5 时,函数值取得最大值,而这里的 x , y 的取值就是我们最后想要得到的结果。
3、相关概念介绍以及实现代码
为了更能方便理解遗传算法,这里需要引进几组概念。
基因型(genotype):性状染色体的内部表现;表现型(phenotype):染色体决定的性状的外部表现,或者说,根据基因型形成的个体的外部表现;进化(evolution):种群逐渐适应生存环境,品质不断得到改良。生物的进化是以种群的形式进行的。适应度(fitness):度量某个物种对于生存环境的适应程度。选择(selection): 以一定的概率从种群中选择若干个个体。一般,选择过程是一种基于适应度的优胜劣汰的过程。复制(reproduction):细胞分裂时,遗传物质DNA通过复制而转移到新产生的细胞中,新细胞就继承了旧细胞的基因。交叉(crossover):两个染色体的某一相同位置处DNA被切断,前后两串分别交叉组合形成两个新的染色体。也称基因重组或杂交;变异(mutation):复制时可能(很小的概率)产生某些复制差错,变异产生新的染色体,表现出新的性状。编码(coding):DNA中遗传信息在一个长链上按一定的模式排列。遗传编码可看作从表现型到基因型的映射。解码(decoding):基因型到表现型的映射。个体(individual):指染色体带有特征的实体;种群(population):个体的集合,该集合内个体数称为种群的大小
3.1 种群和个体
遗传算法是受进化理论启发而形成的。进化是由种群为单位的,种群在生物学上是指在一定空间范围内同时生活着的同种生物的全部个体。显然要想理解种群的概念,又先得理解个体的概念。在遗传算法里,个体通常为某个问题的一个解,并且该解在计算机中被编码为一个向量。例如,上面的例子中要求最大值,所以该问题的解为一组可能的 ( x , y ) 的取值。比如( x = 2.1 , y = 0.8 ) , ( x = − 1.5 , y = 2.3 ) . . . 就是求最大值问题的一个可能解,也就是遗传算法里的个体,把这样的一组一组的可能解的集合就叫做种群。比如在这个问题中设置100个这样的 x , y 的可能的取值对,那么这100个个体就构成了种群。
3.2 编码、解码与染色体
在上面个体概念里提到个体(也就是一组可能解)在计算机程序中被编码为一个向量表示,而在我们这个问题中,个体是x , y 的取值,是两个实数,所以问题就可以转化为如何将实数编码为一个向量表示,编码是为了方便后续操作(交叉和变异)。在计算机中,将不同的实数用二进制串来表示就完成了编码,解码即为编码的逆行为。将个体(可能解)编码后的二进制串叫做染色体,染色体(或者有人叫DNA)就是个体(可能解)的二进制编码表示。
遗传算法中每一条染色体,对应着遗传算法的一个解决方案,一般用适应性函数(fitness function)来衡量这个解决方案的优劣。所以从一个基因组到其解的适应度形成一个映射,可以把遗传算法的过程看作是一个在多元函数里面求最优解的过程。
3.3 适应度和选择
遗传算法是在得到一个种群后根据适者生存规则把优秀的个体保存下来,同时淘汰掉那些不适应环境的个体。那么,如何评价一个个体对环境的适应度?以求解上述函数最大值为例,这个衡量的标准比较容易制定:函数值的大小,即适应度函数直接返回函数值就行了。即直接用可能解(个体)对应的函数的函数值的大小来评估,这样可能解对应的函数值越大越有可能被保留下来。其求解函数最大值的python代码如下:
def get_fitness(pop): x,y = translateDNA(pop)pred = F(x, y)return (pred - np.min(pred)) + 1e-3 #减去最小的适应度是为了防止适应度出现负数,通过这一步fitness的范围为[0, np.max(pred)-np.min(pred)],最后在加上一个很小的数防止出现为0的适应度
pred是将可能解带入函数F中得到的预测值,因为后面的选择过程需要根据个体适应度确定每个个体被保留下来的概率,而概率不能是负值,所以所以最后加上了一个很小的正数。有了求最大值的适应度函数,求最小值适应度函数也就容易了,python代码如下:
def get_fitness(pop): x,y = translateDNA(pop)pred = F(x, y)return (pred - np.max(pred)) + 1e-3
.
有了评估的适应度函数,则可以根据适者生存法则将优秀者保留下来了。选择是根据新个体的适应度进行,但同时不意味着完全以适应度高低为导向(选择 k 个适应度最高的个体,容易陷入局部最优解,因为单纯选择适应度高的个体将可能导致算法快速收敛到局部最优解而非全局最优解,我们称之为早熟)作为折中,遗传算法依据原则:适应度越高,被选择的机会越高,而适应度低的,被选择的机会就低。常用的选择方法――轮盘赌(Roulette Wheel Selection)选择法。比如有5条染色体,他们所对应的适应度评分分别为:5,7,10,13,15,所以所以累计总适应度为:
则各个个体被选中的概率分别为:
可以想象一下,转动轮盘,轮盘停下来的时候,指针会随机地指向某一个个体所代表的区域,很明显,适应度评分越高的个体被选中的概率越大。
遗传算法的选择在python中使用choice实现,np.random.choice函数是numpy库中用于生成随机样本的函数。它可以从一个给定的数组中随机抽取样本。该函数有三个必选参数: a、Size、p。其中a为数组,size为抽取样本的个数,p为每个元素被抽到的概率。还有其他可选参数如replace、weights、random_state等。代码如下:
def select(pop, fitness): # nature selection wrt pop's fitness idx = np.random.choice(np.arange(POP_SIZE), size=POP_SIZE, replace=True, p=(fitness)/(fitness.sum()) ) return pop[idx]
其中,参数p描述了从np.arange(POP_SIZE)里选择每一个元素的概率,概率越高约有可能被选中,最后返回被选中的个体即可。
3.4 交叉、变异
通过选择得到了当前看来“还不错的基因”,但是这并不是最好的基因,需要通过繁殖后代(包含有交叉+变异过程)来产生比当前更好的基因,但是繁殖后代并不能保证每个后代个体的基因都比上一代优秀,这时需要继续通过选择过程来让试应环境的个体保留下来,从而完成进化,不断迭代上面这个过程种群中的个体就会一步一步地进化。具体地繁殖后代过程包括交叉和变异两步。
交叉: 二进制编码的基因交换过程非常类似高中生物中所讲的同源染色体的联会过程――随机把其中几个位于同一位置的编码进行交换,产生新的个体。每一个个体是由父亲和母亲两个个体繁殖产生,子代个体的DNA(二进制串)获得了一半父亲的DNA,一半母亲的DNA,但是这里的一半并不是真正的一半,这个位置叫做交配点,是随机产生的,可以是染色体的任意位置。
变异: 通过交叉子代获得了一半来自父亲一半来自母亲的DNA,但是子代自身可能发生变异,使得其DNA即不来自父亲,也不来自母亲,在某个位置上发生随机改变,通常就是改变DNA的一个二进制位(0变到1,或者1变到0)。例如下面这串二进制编码:
101101001011001
经过基因突变后,可能变成以下这串新的编码:
001101011011001
需要说明的是交叉和变异不是必然发生,而是有一定概率发生。先考虑交叉,最坏情况,交叉产生的子代的DNA都比父代要差(这样算法有可能朝着优化的反方向进行,不收敛),如果交叉是有一定概率不发生,那么就能保证子代有一部分基因和当前这一代基因水平一样;而变异本质上是让算法跳出局部最优解,如果变异时常发生,或发生概率太大,那么算法到了最优解时还会不稳定。交叉概率,范围一般是0.6~1,突变常数(又称为变异概率),通常是0.1或者更小。对于这两个概率,我们一般管叫“步长”。一般来说步长越大,开始时进化的速度会比较快,但是后来比较难收敛到精确的点上。而小步长却能较精确的收敛到一个点上。所以很多时候为了加快遗传算法的进化速度,而又能保证后期能够比较精确地收敛到最优解上面,会采取动态改变步长的方法。这个过程与前面介绍的模拟退火过程比较相类似。
交叉和变异的实现代码如下:
def crossover_and_mutation(pop, CROSSOVER_RATE = 0.8):new_pop = []for father in pop:#遍历种群中的每一个个体,将该个体作为父亲child = father#孩子先得到父亲的全部基因(这里我把一串二进制串的那些0,1称为基因)if np.random.rand() < CROSSOVER_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_popdef mutation(child, MUTATION_RATE=0.003):if np.random.rand() < MUTATION_RATE: #以MUTATION_RATE的概率进行变异mutate_point = np.random.randint(0, DNA_SIZE)#随机产生一个实数,代表要变异基因的位置child[mutate_point] = child[mutate_point]^1 #将变异点的二进制为反转
3.5 群体进化
上述步骤即为遗传算法的核心模块,将这些模块在主函数中迭代起来,即实现种群进化,实现代码如下:
pop = np.random.randint(2, size=(POP_SIZE, DNA_SIZE*2)) #生成种群 matrix (POP_SIZE, DNA_SIZE)for _ in range(N_GENERATIONS):#种群迭代进化N_GENERATIONS代crossover_and_mutation(pop, CROSSOVER_RATE)#种群通过交叉变异产生后代fitness = get_fitness(pop)#对种群中的每个个体进行评估pop = select(pop, fitness) #选择生成新的种群
4、遗传算法流程
4.1 算法步骤
1.评估每条染色体所对应个体的适应度。2.遵照适应度越高,选择概率越大的原则,从种群中选择两个个体作为父方和母方。3.抽取父母双方的染色体,进行交叉,产生子代。4.对子代的染色体进行变异。5.重复2,3,4步骤,直到新种群的产生。选择的作用:优胜劣汰,适者生存;交叉的作用:保证种群的稳定性,朝着最优解的方向进化;变异的作用:保证种群的多样性,避免交叉可能产生的局部收敛。
4.2 遗传算法流程图
。
5、完整代码
import numpy as npimport matplotlib.pyplot as pltfrom matplotlib import cmfrom mpl_toolkits.mplot3d import Axes3DDNA_SIZE = 24 #编码长度POP_SIZE = 200 #种群中个体数量CROSSOVER_RATE = 0.8 # 发生交叉的概率MUTATION_RATE = 0.005 # 发生变异的概率N_GENERATIONS = 50 # 迭代次数X_BOUND = [-3, 3]Y_BOUND = [-3, 3]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)#---------绘制函数3D图--------------def plot_3d(ax): X = np.linspace(*X_BOUND, 100) Y = np.linspace(*Y_BOUND, 100) X,Y = np.meshgrid(X, Y) Z = F(X,Y) ax.plot_surface(X,Y,Z,rstride=1,cstride=1,cmap=cm.coolwarm) ax.set_zlim(-10,10) ax.set_xlabel('x') ax.set_ylabel('y') ax.set_zlabel('z') plt.pause(3) plt.show()#-----------获取适应度------------def get_fitness(pop): x,y = translateDNA(pop) pred = F(x,y) return (pred - np.min(pred)) + 1e-3 # 减去最小的适应度是为了防止适应度出现负数,通过这一步fitness的范围为[0, np.max(pred)-np.min(pred)],最后在加上一个很小的数防止出现为0的适应度#-------------解码---------------def translateDNA(pop): #pop表示种群矩阵,一行表示一个二进制编码表示的DNA,矩阵的行数为种群数目 x_pop = pop[:,1::2]#奇数列表示X y_pop = pop[:,::2] #偶数列表示y # pop:(POP_SIZE,DNA_SIZE)*(DNA_SIZE,1) --> (POP_SIZE,1)完成解码 # x_pop.dot(2**np.arange(DNA_SIZE)[::-1]) 矩阵乘法:x_pop*(2**np.arange(DNA_SIZE)[::-1]) 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,ydef crossover_and_mutation(pop, CROSSOVER_RATE = 0.8): new_pop = [] for father in pop: #遍历种群中的每一个个体,将该个体作为父亲 child = father #孩子先得到父亲的全部基因(这里我把一串二进制串的那些0,1称为基因) if np.random.rand() < CROSSOVER_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_popdef mutation(child, MUTATION_RATE=0.003): if np.random.rand() < MUTATION_RATE: #以MUTATION_RATE的概率进行变异 mutate_point = np.random.randint(0, DNA_SIZE*2) #随机产生一个实数,代表要变异基因的位置 child[mutate_point] = child[mutate_point]^1 #将变异点的二进制为反转#-------选择----------def select(pop, fitness): # nature selection wrt pop's fitness idx = np.random.choice(np.arange(POP_SIZE), size=POP_SIZE, replace=True, p=(fitness)/(fitness.sum()) ) ,,, 介绍以下choice方法的参数: numpy.random.choice(a, size=None, replace=True, p=None) #从a(只要是ndarray都可以,但必须是一维的)中随机抽取数字,并组成指定大小(size)的数组 #replace:True表示可以取相同数字,False表示不可以取相同数字 #数组p:与数组a相对应,表示取数组a中每个元素的概率,默认为选取每个元素的概率相同。 也就是说,从种群中根据适应度函数的大小为挑选概率,挑选POP_SIZE个元素作为下一代 ,,, return pop[idx]def print_info(pop): fitness = get_fitness(pop) max_fitness_index = np.argmax(fitness) 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]))if __name__ == "__main__": fig = plt.figure() ax = Axes3D(fig) plt.ion()#将画图模式改为交互模式,程序遇到plt.show不会暂停,而是继续执行 plot_3d(ax) pop = np.random.randint(2, size=(POP_SIZE, DNA_SIZE*2)) #matrix (POP_SIZE, DNA_SIZE) for _ in range(N_GENERATIONS):#迭代N代 x,y = translateDNA(pop) if 'sca' in locals(): sca.remove() sca = ax.scatter(x, y, F(x,y), c='black', marker='o');plt.show();plt.pause(0.1) pop = np.array(crossover_and_mutation(pop, CROSSOVER_RATE)) #F_values = F(translateDNA(pop)[0], translateDNA(pop)[1])#x, y --> Z matrix fitness = get_fitness(pop) pop = select(pop, fitness) #选择生成新的种群 print_info(pop) plt.ioff() plot_3d(ax)
来源地址:https://blog.csdn.net/qq_50086023/article/details/129103281