遗传算法系列的第三期介绍了如何不用任何框架从零开始解决OneMax问题,第四期介绍了DEAP框架的基本用法。若读者对下文中定义或术语不熟悉,可以查看本系列的前几篇文章。本期文章将介绍如何使用DEAP解决OneMax问题。
1.OneMax问题(OneMax problem)
OneMax问题是遗传算法的入门问题,其内容是:如何使一段长度固定的二进制字符串所有位置上数字之和最大。
让我们用一个长度为5的二进制字符串为例:
- 10010 -> 和为2
- 00111 -> 和为3
- 11111 -> 和为5(最大值)
对一般人,显而易见,当所有位数都为1时,该字符串的和最大,但在我们用遗传算法解决该问题时,算法本身并不具备这样的知识。
2.问题的解决思路
首先,我们得把这个问题转换成一个遗传算法问题,即:我们得定义个体、种群,选择、杂交、突变方法、适应度函数等。假设有一个长度为100的字符串,我们可以做出以下定义:
- 个体:个体即为问题的解,这个问题中个体可以直观的定义为一个长度为100列表(List),列表上每个元素为0或1.
- 种群:种群即所有个体的合集,我们可以把种群定义为所有个体组成的列表。
- 选择:使用锦标赛法(Tournament Selection)
- 杂交:使用单点杂交法(Singe-Point Crossover)
- 突变:使用位翻转突变法(Flip Bit Mutation)
- 适应度函数: 我们的目标是使字符串上所有数字之和最大,适应度函数可以直观的定义为列表中所有数字之和。
若对上述定义不太了解的,可以回看遗传算法系列的第二期。
3.用DEAP框架实现遗传算法
以下将分步骤解释每一部分的代码,完整代码在本文的最后可见。
3.1. 准备工作
# 1.load modulesfrom deap import base,creator,tools,algorithmsimport randomimport numpy as npimport matplotlib.pyplot as plttoolbox = base.Toolbox()
首先我们需要导入遗传算法所必须的模组:
- base,creator,tools,algorithms: 这四个模组是DEAP框架内最常用的模组
- random: 用以生成随机数
- numpy:我们将用到 numpy 中的 mean 和 max 方程。
- matplotlib: 用以绘图
- toolbox:DEAP框架中的核心,当使用deap.algorithms时,我们需要把遗传算法的运算符存入toolbox内。 同时toolbox.register也是DEAP中最常用的方法。
3.2. 初始化算法参数
# 2.parameters:INDIVIDUAL_LENGTH = 100 # length of bit string to be optimizedPOPULATION_SIZE = 200P_CROSSOVER = 0.9 # probability for crossoverP_MUTATION = 0.1 # probability for mutating an individualMAX_GENERATIONS = 50random.seed(39)
- INDIVIDUAL_LENGTH:个体的“长度”,即二进制字符串的长度
- POPULATION_SIZE: 种群中个体的数目
- P_CROSSOVER: 个体间交杂的概率
- P_MUTATION: 个体突变的概率
- MAX_GENERATIONS: 算法迭代次数上限
- random.seed:为了保证每次运行的结果相同,设定了随机种子
3.3. 定义个体与种群
# 3.create individual and populationtoolbox.register("genBinary", random.randint, 0, 1) # 1creator.create("FitnessMax", base.Fitness, weights=(1.0,)) # 2creator.create("Individual", list, fitness=creator.FitnessMax) # 3toolbox.register("createIndividual", tools.initRepeat, creator.Individual, toolbox.genBinary, INDIVIDUAL_LENGTH) # 4toolbox.register("createPopulation", tools.initRepeat, list, toolbox.createIndividual) # 5
- 这一列定了genBinary方程:该函数可以随机生成0或1;通过toolbox.register()定义后,我们可以通过toolbox.genBinary()直接使用该函数。
- 因为在OneMax问题中,我们需要适应度的最大值,所以我们拓展base.Fitness类,并将拓展后的类命名为FitnessMax,并设定其权重为(1.0,),代表求最大值(在其他问题中,如果需要求最小值的,可以定义权重为(-1.0,))。
- 在这里注意我们定义权重是(1.0,)而不是1.0,其原因是DEAP框架支持求多个目标的的最大/最小值,所以我们必须定义权重为元组(tuple)。
- 当在其他问题中我们的适应度方程有多个目标:如当我们需要第一个目标最大,同时第二个目标最小,且第一个目标的重要性是第二个目标的两倍时,权重可以定义为:(1.0,-0.5)
- 创建Individual类,并让FitnessMax做为Individual的fitness特性(attribute)
- 定义createIndividual方程:该方程可以生成长度为INDIVIDUAL_LENGTH的列表,通过调用genBinary函数,列表内每个元素随机为0或1.
- 定义createPopulation方程:该方程可以生成长度待定的列表,列表中每个元素由createIndividual方程创建。
- 注意createPopulation方程并没有定义列表的长度,这是因为toolbox.register中用到了functools.partial方程,尚未定义的参数会以*args或**kwargs的形式传递给tools.initRepeat方程。
如果读者对DEAP框架内的方程不太了解,看完上述解释后还是感觉五里雾中,建议查看本系列前两期文章,或者到DEAP官网上多了解下这些方程的定义。
3.4. 定义适应度函数
# 4. define evaluation functiontoolbox.register("evaluate", lambda ind: (sum(ind),))
在OneMax问题中,适应度函数即为个体(用二进制字符串表示)上所有数字之和,因此我们将适应度函数定义为(sum(ind),)。
在这里请特别注意:
- 因为DEAP框架支持有多个目标的适应度函数,所以我们必须把适应度函数的输出定义为元组。
- evaluate是关键词,若要使用DEAP框架,必须使用evaluate这个单词来命名适应度函数。
3.5. 定义遗传算法运算符(选择,杂交,突变)
# 5. define operatorstoolbox.register("select", tools.selTournament, tournsize=2)toolbox.register("mate", tools.cxOnePoint)toolbox.register("mutate", tools.mutFlipBit, indpb=1.0/INDIVIDUAL_LENGTH)
- select:锦标赛法作为选择运算符,tournsize表示锦标赛的大小
- mate:单点杂交作为杂交运算符
- mutate:多位翻转突变作为突变运算符,indpb表示了每一个位置上基因突变的概率。在本文中INDIVIDUAL_LENGTH默认为100, 即每个体格由一百个二进制字符组成,每个字符突变的概率为1%
请特别注意:
select, mate, mutate都是DEAP框架中的关键词,在定义运算符时,必须使用使用这几个单词。
3.6. 运行遗传算法
# Genetic Algorithm flow:def main(): # create population population = toolbox.createPopulation(n=POPULATION_SIZE) # initialize statistics stats = tools.Statistics(lambda ind: ind.fitness.values) stats.register("max", np.max) stats.register("avg", np.mean) # Genetic Algorithm population, logbook = algorithms.eaSimple(population, toolbox, cxpb=P_CROSSOVER, mutpb=P_MUTATION, ngen=MAX_GENERATIONS,stats=stats, verbose=True) # gather statistics maxFitnessValues, meanFitnessValues = logbook.select("max", "avg") # plot statistics: plt.plot(maxFitnessValues, color='red',label="Max Fitness") plt.plot(meanFitnessValues, color='green',label="Average Fitness") plt.legend() plt.xlabel('Generation') plt.ylabel('Fitness') plt.title('Max and Average Fitness over Generations') plt.show() print(max(population,key=lambda ind:sum(ind)))if __name__ == "__main__": main()
在上述代码中,我们定义了main()方程,运行该方程就可以进行遗传算法的运算,同时生成对应的统计数据与图表。
该方程中有以下步骤:
- 创建种群:种群大小为POPULATION_SIZE
- 初始化stats: 把个体的适应度收集进tools.Statistics对象。
- 通过algorithms.eaSimple方程运行遗传算法,并将最终的种群与统计数据分别存入population和logbbok变量:
- population:初始的种群
- toolbox:toolbox变量,该变量内包括了我们之前定义的evaluate, select, mate 和 mutate方程
- cxpb:杂交概率
- mutpb:突变概率
- ngen:最大迭代代数
- stats:Statistics对象,用以储存迭代过程中每一代种群的数据
- verbose:在迭代过程中是否将Statistics输出至标准输出
- 从logbook变量中获取每一代适应度的最大值和均值
- 使用收集的数据画图,并显示适应度最高的个体,即最优解。
4. 运算结果
由下图所示,在第38代算法已经产生了最优解,即个体所有位置上都为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, 1]
当我们设置verbose=True时,algorithms.eaSimple会在迭代过程中实时生成每一代的数据并输出至标准输出:
gen nevals max avg 0 200 65 50.351 191 62 52.9052 175 65 55.2253 189 67 57.3454 169 68 59.25 5 176 72 61.2856 170 73 63.3457 182 73 65.1658 179 74 66.8359 181 78 68.47510 188 80 70.13 11 178 82 71.95 12 189 81 73.75513 188 82 75.14514 194 83 76.75515 176 83 78.29 16 182 83 79.35 17 169 85 80.34 18 177 86 81.37 19 178 86 82.43520 184 87 83.31 21 179 88 84.17522 179 89 84.83523 179 90 85.57524 188 91 86.52525 189 93 87.55 26 181 94 88.44 27 190 94 89.31 28 185 95 89.94 29 174 96 90.7 30 188 98 91.41 31 180 98 92.19532 177 98 93.00533 179 98 93.83 34 175 98 94.55535 185 99 95.16536 179 99 95.72537 188 99 96.34538 182 100 97.0439 184 100 97.6340 183 100 98.0341 177 100 98.3742 192 100 98.68543 186 100 98.98544 175 100 99.32545 187 100 99.56546 171 100 99.7347 180 100 99.8948 186 100 99.8749 189 100 99.9150 187 100 99.9
5. 小结
本文中介绍了如何使用DEAP框架来解决OneMax问题,本系列的接下来几篇文中,我将详细解释algorithms.eaSimple如何运作,以及如何自定义tools.Statistics和logbook。
6. 完整代码
# 1.load modulesfrom deap import base,creator,tools,algorithmsimport randomimport numpy as npimport matplotlib.pyplot as plttoolbox = base.Toolbox()# 2.parameters:INDIVIDUAL_LENGTH = 100 # length of bit string to be optimizedPOPULATION_SIZE = 200P_CROSSOVER = 0.9 # probability for crossoverP_MUTATION = 0.1 # probability for mutating an individualMAX_GENERATIONS = 50random.seed(39)# 3.create individual and populationtoolbox.register("genBinary", random.randint, 0, 1)creator.create("FitnessMax", base.Fitness, weights=(1.0,))creator.create("Individual", list, fitness=creator.FitnessMax)toolbox.register("createIndividual", tools.initRepeat, creator.Individual, toolbox.genBinary, INDIVIDUAL_LENGTH)toolbox.register("createPopulation", tools.initRepeat, list, toolbox.createIndividual)# 4. define evaluation functiontoolbox.register("evaluate", lambda ind: (sum(ind),))# 5. define operatorstoolbox.register("select", tools.selTournament, tournsize=2)toolbox.register("mate", tools.cxOnePoint)toolbox.register("mutate", tools.mutFlipBit, indpb=1.0/INDIVIDUAL_LENGTH)# Genetic Algorithm flow:def main(): # create population population = toolbox.createPopulation(n=POPULATION_SIZE) # initialize statistics stats = tools.Statistics(lambda ind: ind.fitness.values) stats.register("max", np.max) stats.register("avg", np.mean) # Genetic Algorithm population, logbook = algorithms.eaSimple(population, toolbox, cxpb=P_CROSSOVER, mutpb=P_MUTATION, ngen=MAX_GENERATIONS,stats=stats, verbose=True) # gather statistics maxFitnessValues, meanFitnessValues = logbook.select("max", "avg") # plot statistics: plt.plot(maxFitnessValues, color='red',label="Max Fitness") plt.plot(meanFitnessValues, color='green',label="Average Fitness") plt.legend() plt.xlabel('Generation') plt.ylabel('Fitness') plt.title('Max and Average Fitness over Generations') plt.show()if __name__ == "__main__": main()