运算符和算法

在开始复杂的算法之前,我们将了解一些DEAP的基础知识。首先,我们将从创建简单的个体(如在 创建类型 教程中看到的)开始,并使用不同的操作符使它们相互作用。之后,我们将学习如何使用算法和其他工具。

第一个个体

首先导入所需的模块,并注册创建具有最小化两个目标适应度的浮点数列表个体的不同函数。

import random

from deap import base
from deap import creator
from deap import tools

IND_SIZE = 5

creator.create("FitnessMin", base.Fitness, weights=(-1.0, -1.0))
creator.create("Individual", list, fitness=creator.FitnessMin)

toolbox = base.Toolbox()
toolbox.register("attr_float", random.random)
toolbox.register("individual", tools.initRepeat, creator.Individual,
                 toolbox.attr_float, n=IND_SIZE)

现在可以通过在脚本中添加适当的行来构建第一个个体。

ind1 = toolbox.individual()

打印单独的 ind1 并检查其适应度是否有效将得到类似这样的结果

print(ind1)               # [0.86..., 0.27..., 0.70..., 0.03..., 0.87...]
print(ind1.fitness.valid) # False

个体以其基类表示形式(此处为列表)打印,并且适应度无效,因为它不包含任何值。

评估

评估是进化算法中最个人化的部分,它是您必须自己编写的库的唯一部分。典型的评估函数接受一个个体作为参数,并将其适应度作为 tuple 返回。如 core 部分所示,适应度是一个浮点值列表,并且有一个属性 valid 来判断这个个体是否需要重新评估。适应度通过将 values 设置为相应的 tuple 来设定。例如,以下代码评估了之前创建的个体 ind1 并将其适应度赋值给相应的值。

def evaluate(individual):
    # Do some hard computing on the individual
    a = sum(individual)
    b = len(individual)
    return a, 1. / b

ind1.fitness.values = evaluate(ind1)
print(ind1.fitness.valid)    # True
print(ind1.fitness)          # (2.73, 0.2)

处理单目标适应度并无不同,评估函数 必须 返回一个元组,因为单目标是多目标的一个特例。

突变

我们将介绍的下一种运算符是变异运算符。在 deap.tools 模块中有多种变异运算符。每种变异都有其自身的特性,可能适用于不同类型的个体。请仔细阅读所选运算符的文档,以避免不期望的行为。

变异操作符的一般规则是它们 进行变异,这意味着如果需要保留原始个体或该个体是另一个个体的 *引用*(参见选择操作符),则必须在变异之前创建一个独立的副本。

为了对个体 ind1 应用突变(这里是一个高斯突变),只需应用所需函数。

mutant = toolbox.clone(ind1)
ind2, = tools.mutGaussian(mutant, mu=0.0, sigma=0.2, indpb=0.2)
del mutant.fitness.values

适应度值被删除,因为它们与个体不再相关。如上所述,变异只对个体进行变异,不负责使适应度失效或任何其他事情。以下显示 ind2mutant 实际上是同一个个体。

print(ind2 is mutant)    # True
print(mutant is ind1)    # False

交叉

我们将介绍的第二种运算符是交叉运算符。在 deap.tools 模块中有多种交叉运算符。每个交叉运算符都有其自身的特点,可能适用于不同类型的个体。请仔细阅读所选运算符的文档,以避免不期望的行为。

交叉操作符的一般规则是它们 配对个体,这意味着如果需要保留原始个体或它们是其他个体的 *引用*(参见选择操作符),那么在配对个体之前必须制作独立的副本。

让我们对两个事先克隆的子代应用交叉操作。

child1, child2 = [toolbox.clone(ind) for ind in (ind1, ind2)]
tools.cxBlend(child1, child2, 0.5)
del child1.fitness.values
del child2.fitness.values

备注

关于语言的一个说明,形式 toolbox.clone([ind1, ind2]) 不能使用,因为如果 ind1ind2 指向内存中的同一位置(同一个个体),将会有一个独立的个体副本,而第二个将是对此同一独立副本的引用。这是由防止递归循环的机制引起的。第一次看到该个体时,它被放入“备忘录”字典中,下次再看到时,该对象的深度复制将停止,并放入对之前创建的深度副本的引用。在深度复制容器时应小心。

选择

选择是通过 deap.tools 模块中可用的选择操作符在一群个体中进行的。选择操作符通常将一个个体可迭代容器和要选择的个体数量作为第一个参数。它返回一个包含所选个体引用的列表。选择过程如下。

selected = tools.selBest([child1, child2], 2)
print(child1 in selected)	# True

警告

这里**非常**重要的是要注意,选择操作符在选择过程中不会复制任何个体。如果一个个体被选择了两次,并且其中一个对象被修改,另一个也会被修改。只有对个体的引用被复制。就像其他所有操作符一样,它只选择,仅此而已。

通常在选择之后或变异之前,会对整个种群进行复制。

selected = toolbox.select(population, LAMBDA)
offspring = [toolbox.clone(ind) for ind in selected]

使用工具箱

该工具箱旨在包含所有进化工具,从对象初始化器到评估操作符。它允许轻松配置每个算法。工具箱基本上有两种方法,register()unregister(),用于向工具箱添加或移除工具。

本教程的这一部分将重点介绍进化工具在工具箱中的注册,而不是初始化工具。进化工具的常用名称是 mate()mutate()evaluate()select(),然而,任何名称都可以注册,只要它是唯一的。以下是如何在工具箱中注册它们。

from deap import base
from deap import tools

toolbox = base.Toolbox()

def evaluateInd(individual):
    # Do some computation
    return result,

toolbox.register("mate", tools.cxTwoPoint)
toolbox.register("mutate", tools.mutGaussian, mu=0, sigma=1, indpb=0.2)
toolbox.register("select", tools.selTournament, tournsize=3)
toolbox.register("evaluate", evaluateInd)

使用工具箱注册工具有助于保持其他算法与操作集的独立性。使用这种方案使得在需要时定位和更改工具箱中的任何工具变得非常容易。

使用工具

在构建进化算法时,工具箱用于包含操作符,这些操作符通过其通用名称调用。例如,这里是一个非常简单的世代进化算法。

for g in range(NGEN):
    # Select the next generation individuals
    offspring = toolbox.select(pop, len(pop))
    # Clone the selected individuals
    offspring = map(toolbox.clone, offspring)

    # Apply crossover on the offspring
    for child1, child2 in zip(offspring[::2], offspring[1::2]):
        if random.random() < CXPB:
            toolbox.mate(child1, child2)
            del child1.fitness.values
            del child2.fitness.values

    # Apply mutation on the offspring
    for mutant in offspring:
        if random.random() < MUTPB:
            toolbox.mutate(mutant)
            del mutant.fitness.values

    # Evaluate the individuals with an invalid fitness
    invalid_ind = [ind for ind in offspring if not ind.fitness.valid]
    fitnesses = toolbox.map(toolbox.evaluate, invalid_ind)
    for ind, fit in zip(invalid_ind, fitnesses):
        ind.fitness.values = fit

    # The population is entirely replaced by the offspring
    pop[:] = offspring

这是一个完整的算法。它足够通用,可以接受任何类型的个体和任何操作符,只要这些操作符适合所选的个体类型。如最后一个示例所示,工具箱的使用使得编写的算法尽可能接近伪代码。现在轮到你自己编写和实验了。

工具装饰

工具装饰是一个非常强大的功能,它有助于在进化过程中控制非常精确的事物,而无需更改算法或操作符中的任何内容。装饰器是一个包装器,它被调用来替代函数。它被要求在实际函数调用之前和之后进行一些初始化和终止工作。例如,在约束域的情况下,可以对变异和交叉应用装饰器,以确保任何个体都不会超出边界。以下定义了一个装饰器,用于检查列表中的任何属性是否超出边界,并在这种情况下对其进行裁剪。装饰器使用三个函数定义,以便接收 minmax 参数。每当调用变异或交叉时,都会对结果个体进行边界检查。

def checkBounds(min, max):
    def decorator(func):
        def wrapper(*args, **kargs):
            offspring = func(*args, **kargs)
            for child in offspring:
                for i in range(len(child)):
                    if child[i] > max:
                        child[i] = max
                    elif child[i] < min:
                        child[i] = min
            return offspring
        return wrapper
    return decorator

toolbox.register("mate", tools.cxBlend, alpha=0.2)
toolbox.register("mutate", tools.mutGaussian, mu=0, sigma=2)

toolbox.decorate("mate", checkBounds(MIN, MAX))
toolbox.decorate("mutate", checkBounds(MIN, MAX))

这将在交叉和变异上起作用,因为两者都返回一个个体元组。变异通常被认为返回一个单独的个体,但同样地,就像在评估中一样,单一个体的情况是多个个体情况的一个特例。

|更多| 关于装饰器的更多信息,请参见 Python 装饰器简介Python 装饰器库

变体

变体允许使用预定义的小构建块来构建简单的算法。为了使用一个变体,工具箱必须包含所需的运算符。例如,在最后展示的完整算法中,交叉和变异被分组在 varAnd() 函数中,这个函数要求工具箱包含 mate()mutate() 函数。这种变体可以用来简化算法的编写,如下所示。

from deap import algorithms

for g in range(NGEN):
    # Select and clone the next generation individuals
    offspring = map(toolbox.clone, toolbox.select(pop, len(pop)))

    # Apply crossover and mutation on the offspring
    offspring = algorithms.varAnd(offspring, toolbox, CXPB, MUTPB)

    # Evaluate the individuals with an invalid fitness
    invalid_ind = [ind for ind in offspring if not ind.fitness.valid]
    fitnesses = toolbox.map(toolbox.evaluate, invalid_ind)
    for ind, fit in zip(invalid_ind, fitnesses):
        ind.fitness.values = fit

    # The population is entirely replaced by the offspring
    pop[:] = offspring

最后一个示例展示了使用这些变体可以直观地构建非常接近伪代码的算法。

算法

algorithms 模块中实现了几种算法。它们非常简单,反映了文献中存在的基本进化算法类型。这些算法使用上一节中定义的 Toolbox。为了为算法设置工具箱,您必须在指定的名称下注册所需的运算符,请参阅所选算法的文档以获取更多详细信息。一旦工具箱准备就绪,就可以启动算法了。简单的进化算法需要 5 个参数,一个 population*(种群),一个 *toolbox*(工具箱),每代个体交配的概率(*cxpb),每代个体变异的概率(mutpb),以及要完成的代数(ngen)。

from deap import algorithms

algorithms.eaSimple(pop, toolbox, cxpb=0.5, mutpb=0.2, ngen=50)

理解简单进化算法作用的最佳方式是查看文档或源代码。

既然你已经在Python中构建了自己的进化算法,欢迎你给我们反馈和赞赏。我们也非常想听听你的项目以及使用DEAP的成功故事。