遗传算法实践详解(deap框架初体验)

本文已参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。

OneMax问题介绍

OneMax问题是一个简单的优化任务,通常作为遗传算法Hello World。 OneMax任务是查找给定长度的二进制串,最大化该二进制串中数字的总和。例如,对于长度为5的OneMax问题,10010的数字总和为2,01110的数字总和为3。 显然,此问题的最优解为每位数字均为1的二进制串。但是对于遗传算法而言,其并不具备此知识,因此需要使用其遗传算子寻找最优解。算法的目标是在合理的时间内找到最优解,或者至少找到一个近似最优解

遗传算法实践

在进行实践前,应首先明确遗传算法中所用到的要素定义。

  • 选择染色体 由于OneMax问题涉及二进制串,因此每个个体的染色体直接利用代表候选解的二进制串表示是一个自然的选择。在Python中,将其实现为仅包含0/1整数值的列表。染色体的长度与OneMax问题的规模匹配。例如,对于规模为5的OneMax问题,个体10010由列表[1,0,0,1,0]表示。

  • 适应度的计算 由于要最大化该二进制串中数字的总和,同时由于每个个体都由0/1整数值列表表示,因此适合度可以设计为列表中元素的总和,例如:sum([1,0,0,1,0])= 2。

  • 选择遗传算子 选择遗传算子并没有统一的标准,通常可以尝试几种选择方案,找到最适合的方案。其中选择算子通常可以处理任何染色体类型,但是交叉和突变算子通常需要匹配使用的染色体类型,否则可能会产生无效的染色体。

    • 选择算子:此处选用锦标赛选择。
    • 交叉算子:此处选用单点交叉。
    • 突变算子:此处选用位翻转突变。
  • 设定停止条件 限制繁衍的代际数通常是一个不错的停止条件,它可以确保算法不会永远运行。 另外,由于我们知道了OneMax问题的最佳解决方案(一个全为1的二进制串,也就是说其适应度等于代表个体的列表长度),因此可以将其用作另一个停止条件。Note. 但是,对于现实世界中的多数问题而言,通常不存在这种可以公式化精确定义的先验知识。

遗传算法要素配置

在开始实际的遗传算法流程之前,需要根据上述要素的设计利用代码实现:

  1. 首先导入所用包:
from deap import base
from deap import creator
from deap import tools
import random
import matplotlib.pyplot as plt
复制代码
  1. 接下来,声明一些常量,这些常量用于设置OneMax问题的参数并控制遗传算法的行为:
#超参数声明
ONE_MAX_LENGTH = 100    #length of bit string to be optimized
POPULATION_SIZE = 200   #number of individuals in population
P_CROSSOVER = 0.9       #probability for crossover
P_MUTATION = 0.1        #probability for mutating an individual
MAX_GENERATION = 50     #max number of generations for stopping condition
复制代码
  1. 接下来,使用Toolbox类创建zeroOrOne操作,该操作用于自定义random.randomint(a,b)函数。通过将参数a和b固定为值0和1,当在调用此运算时时,zeroOrOne运算符将随机返回0或1。以下代码段,然后使用它来注册:
toolbox = base.Toolbox()#定义toolbox变量
toolbox.register("zeroOrOne",random.randint,0,1)#注册zeroOrOne运算
复制代码
  1. 接下来,需要创建Fitness类。由于这里只有一个目标——最大化数字总和,因此选择FitnessMax策略,使用具有单个正权重的权重元组:
creator.create("FitnessMax",base.Fitness,weights=(1.0,))
复制代码
  1. deap中,通常使用“Individual”的类来代表种群中的每个个体。使用creator工具创建该类,使用列表作为基类,用于表示个体的染色体。并为该类增加Fitness属性,该属性初始化为步骤4中定义的FitnessMax类:
creator.create("Individual", list, fitness=creator.FitnessMax)
复制代码
  1. 接下来,注册individualCreator操作,该操作创建Individual类的实例,并利用步骤1中自定义的zeroOrOne操作随机填充0/1。注册individualCreator操作使用基类initRepeat操作,并使用以下参数对基类进行实例化:

(1) 将creator.Individual类作为放置结果对象的容器类型 (2) zeroOrOne操作是生成对象的函数 (3) 常量ONE_MAX_LENGTH作为要生成的对象数目 由于zeroOrOne运算符生成的对象是0/1的随机数,因此,所得的individualCreator运算符将使用100个随机生成的0或1填充单个实例:

toolbox.register("individualCreator",tools.initRepeat,creator.Individual,toolbox.zeroOrOne,ONE_MAX_LENGTH)
复制代码
  1. 最后,注册用于创建个体列表的populationCreator操作。该定义使用带有以下参数的initRepeat基类操作

(1) 将列表类作为容器类型 (2) 用于生成列表中对象的函数——personalCreator运算符 这里没有传入initRepeat的最后一个参数——要生成的对象数量。这意味着,当使用populationCreator操作时,需要指定此参数用于确定创建的个体数:

toolbox.register("populationCreator",tools.initRepeat,list,toolbox.individualCreator)
复制代码
  1. 为了简化适应度(在deap中称为evaluation)的计算,首先定义一个独立的函数,该函数接受Individual类的实例并返回其适应度。

这里定义oneMaxFitness函数,用于计算个体中1的数量。

def oneMaxFitness(individual):
    return sum(individual),#deap中的适用度表示为元组,因此,当返回单个值时,需要用逗号将其声明为元组。
复制代码
  1. 接下来,将evaluate运算符定义为oneMaxfitness()函数的别名。使用evaluate别名来计算适应度是deap的一种约定:
toolbox.register("evaluate",oneMaxFitness)
复制代码
  1. 遗传算子通常是通过对tools模块中的现有函数进行别名命名,并根据需要设置参数值来创建的。根据上节设计的要素创建遗传算子。
toolbox.register("select",tools.selTournament,tournsize=3)
toolbox.register("mate",tools.cxOnePoint)
# mutFlipBit函数遍历个体的所有特征,并且对于每个特征值,
# 都将使用indpb参数值作为翻转(应用not运算符)该特征值的概率。
# 该值与突变概率无关,后者由P_MUTATION常数设置。
# 突变概率用于确定是否为种群中的给定个体调用mutFlipBit函数
toolbox.register("mutate",tools.mutFlipBit,indpb=1.0/ONE_MAX_LENGTH)
复制代码

遗传算法解的进化

遗传流程如以下步骤所示:

  1. 通过使用之前定义的populationCreator操作创建初始种群,并以POPULATION_SIZE常量作为该操作的参数。并初始化generationCounter变量,用于判断代际数:
population = toolbox.populationCreator(n=POPULATION_SIZE)
generationCounter = 0
复制代码
  1. 为了计算初始种群中每个个体的适应度,使用map()函数将evaluate操作应用于种群中的每个个体。由于evaluate操作是oneMaxFitness()函数的别名,因此,迭代的结果由每个个体的计算出的适应度元组组成。 得到结果后将其转换为元组列表:
fitnessValues = list(map(toolbox.evaluate,population)
复制代码
  1. 由于fitnessValues的项分别与population(个体列表)中的项匹配,因此可以使用zip()函数将它们组合并为每个个体分配相应的适应度元组:
for individual,fitnessValue in zip(population,fitnessValues):
	individual.fitness.values = fitnessValue
复制代码
  1. 接下来,由于适应度元组仅有一个值,因此从每个个体的适应度中提取第一个值以获取统计数据:
fitnessValues = [indivalual.fitness.values[0] for individual in population]
复制代码
  1. 统计种群每一代的最大适应度和平均适应度。创建两个列表用于存储统计值:
maxFitnessValues = []
meanFitnessValues = []
复制代码
  1. 遗传流程的主要准备工作已经完成,在循环时还需设置停止条件,通过限制代际数来设置一个停止条件,而通过检测是否达到了最佳解(所有二进制串位都为1)作为另一个停止条件:
while max(fitnessValues)<ONE_MAX_LENGTH and generationCounter<MAX_GENERATIONS:
复制代码
  1. 接下来更新代际计数器:
	generationCounter = generationCounter + 1
复制代码
  1. 遗传算法的核心是遗传运算符。第一个是selection运算符,使用先前利用toolbox.select定义的锦标赛选择。由于我们已经在定义运算符时设置了锦标赛大小,因此只需要将物种及其长度作为参数传递给选择运算符:
	offspring = toolbox.select(population,len(population))
复制代码
  1. 被选择的个体被赋值给offspring变量,接下来将其克隆,以便我们可以应用遗传算子而不影响原始种群:

Note. 尽管命名为offspring,但它们仍然是前一代的个体的克隆,我们仍然需要使用crossover运算符将它们配对以创建实际的后代。

	offspring = list(map(toolbox.clone,offspring)
复制代码
  1. 下一个遗传算子是交叉。已经在上节中定义为toolbox.mate运算符,并且其仅仅是单点交叉的别名。使用Python切片将offspring列表中的每个偶数索引项与奇数索引项对作为双亲。然后,以P_CROSSOVER常数设置的交叉概率进行交叉。这将决定这对个体是会交叉或保持不变。最后,删除后代的适应度值,因为它们现有的适应度已经不再有效:
	for child1,child2 in zip(offspring[::2],offspring[1::2]):
		if random.random() < P_CROSSOVER:
			toolbox.mate(child1,child2)
			del child1.fitness.values
			del child2.fitness.values
复制代码
  1. 最后一个遗传运算符是突变,先前已注册为toolbox.mutate运算符,并设置为翻转位突变操作。遍历所有后代项,将以由突变概率常数P_MUTATION设置的概率应用变异算子。如果个体发生突变,我们确保删除其适应性值。由于该值可能继承自上一代,并且在突变后不再正确,需要重新计算:
	for mutant in offspring:
		if random.random() < P_MUTATION:
			toolbox.mutate(mutant)
			del mutant.fitness.values
复制代码
  1. 没有交叉或变异的个体保持不变,因此,它们的现有适应度值(已在上一代中计算出)就无需再次计算。其余个体的适应度值为空。使用“Fitness”类的valid属性查找这些新个体,然后以与原始适应性值计算相同的方式为其计算新适应性:
	freshIndividuals = [ind for ind in offspring if not ind.fitness.valid]
	freshFitnessValues = list(map(toolbox.evaluate,freshIndividuals))
	for individual,fitnessValue in zip(freshIndividuals,freshFitnessValues):
		individual.fitness.values = fitnessValue
复制代码
  1. 遗传算子全部完成后,就可以使用新的种群取代旧的种群了:
	population[:] = offspring
复制代码
  1. 在继续下一轮循环之前,将使用与上述相同的方法统计当前的适应度值以更新统计信息:
	fitnessValues = [ind.fitness.values[0] for ind in population]
复制代码
  1. 获得最大和平均适应度值,将它们的值添加到统计列表中:
	maxFitness = max(fitnessValues)
	meanFitness = sum(fitnessValues) / len(population)
	maxFitnessValues.append(maxFItness)
	meanFItnessValues.append(meanFItness)
	print(“- Generation {}: Max Fitness = {}, Avg Fitness = {}”.format(generationCounter,
	maxFitness, meanFitness)
复制代码
  1. 此外,使用得到的最大适应度值来找到最佳个体的索引,并打印出该个体:
	best_index = fitnessValues.index(max(fitnessValues))
	print(“Best Individual = “, *population[best_index], “\n”)
复制代码
  1. 满足停止条件并且遗传算法流程结束后,可以使用获取的统计信息,使用matplotlib库可视化算法执行过程中的统计信息,展示各代个体的最佳和平均适应度值的变化:
plt.plot(maxFitnessValues,color=’red’)
plt.plot(meanFitnessValues,color=’green’)
plt.xlabel(‘Generation’)
plt.ylabel(‘Max / Average Fitness’)
plt.title(‘Max and Average fitness over Generation’)
plt.show()
复制代码

该部分完整代码如下:

def main():
    population = toolbox.populationCreator(n=POPULATION_SIZE)
    generationCounter = 0
    fitnessValues = list(map(toolbox.evaluate,population))

    for individual,fitnessValue in zip(population,fitnessValues):
        individual.fitness.values = fitnessValue

    fitnessValues = [individual.fitness.values[0] for individual in population]

    maxFitnessValues = []
    meanFitnessValues = []

    while max(fitnessValues) < ONE_MAX_LENGTH and generationCounter < MAX_GENERATION:
        generationCounter = generationCounter + 1
        
        offspring = toolbox.select(population,len(population))
        offspring = list(map(toolbox.clone,offspring))

        for child1,child2 in zip(offspring[::2],offspring[1::2]):
            if random.random() < P_CROSSOVER:
                toolbox.mate(child1,child2)
                del child1.fitness.values
                del child2.fitness.values
        
        for mutant in offspring:
            if random.random() < P_MUTATION:
                toolbox.mutate(mutant)
                del mutant.fitness.values
        
        freshIndividuals = [ind for ind in offspring if not ind.fitness.valid]
        freshFitnessValues = list(map(toolbox.evaluate,freshIndividuals))
        for individual,fitnessValue in zip(freshIndividuals,freshFitnessValues):
            individual.fitness.values = fitnessValue
        
        population[:] = offspring

        fitnessValues = [ind.fitness.values[0] for ind in population]

        maxFitnessValue = max(fitnessValues)
        meanFitnessValue = sum(fitnessValues) / len(population)
        maxFitnessValues.append(maxFitnessValue)
        meanFitnessValues.append(meanFitnessValue)
        print("- Generation {}: Max Fitness = {}, Avg Fitness = {}".format(generationCounter,maxFitnessValue,meanFitnessValue))

        best_index = fitnessValues.index(max(fitnessValues))
        print("Best Indivadual = ", *population[best_index],"\n")

    plt.plot(maxFitnessValues,color="red")
    plt.plot(meanFitnessValues,color="green")
    plt.xlabel("Generation")
    plt.ylabel("Max / Average Fitness")
    plt.title("Max and Average fitness over Generation")
    plt.show()
复制代码

至此可以开始测试我们的遗传算法了,运行代码以验证其是否找到了OneMax问题的最优解。

算法运行

if __name__ == "__main__":
    main()
复制代码

运行程序时,可以看到程序运行输出:

- Generation 27: Max Fitness = 99.0, Avg Fitness = 96.805
Best Indivadual =  1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 

- Generation 28: Max Fitness = 99.0, Avg Fitness = 97.235
Best Indivadual =  1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 

- Generation 29: Max Fitness = 99.0, Avg Fitness = 97.625
Best Indivadual =  1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 

- Generation 30: Max Fitness = 100.0, Avg Fitness = 98.1
Best Indivadual =  1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 
复制代码

可以看到30代后算法找到全1解,结果适应度为100,并停止了遗传流程。平均适应度开始时仅为53左右,结束时接近100。 绘制图形如下所示:

onemax.png

该图说明了最大适应度与平均适应度是如何随着代数的增加而逐步增加。

猜你喜欢

转载自juejin.im/post/7019505193255436319