pythonmathematical-optimizationgenetic-algorithmdeap

DEAP: make fitness of an individual depend on entire population


To implement a sharing strategy (described here), the fitness function needs to depend on other individuals in the population.

def shared_fitness(individual, population):
    #compute
    return result

How do I register this function in a toolbox to be used by an genetic algorithm? In order to do this:

toolbox = base.Toolbox()
toolbox.register('evaluate', shared_fitness, population=pop)

I first have to define a population pop. But during the algorithm epochs, I want the fitness to be evaluated with the current population, not the initial population.

How can I implement a fitness function that depends on other individuals in the population?


Solution

  • notice that in 99% of the time our population will be some sort of list (or other container object). When we pass these objects into functions we pass the pointer and not the values. This means that any changes we make to the population will affect the evaluation function, which holds a pointer to the population.
    For a sanity test I took the N-Queens example from DEAP and made a small change to the evaluation function - just to print the current top-5 population members. When you run this you can see that the output changes, even though the evaluation function received the "initial population" as input.

    If for some reason your population is passed by value and not by pointer, maybe a global variable containing the current population at all times could help, although this is less preferable, of course.

    #    This file is part of DEAP.
    #
    #    DEAP is free software: you can redistribute it and/or modify
    #    it under the terms of the GNU Lesser General Public License as
    #    published by the Free Software Foundation, either version 3 of
    #    the License, or (at your option) any later version.
    #
    #    DEAP is distributed in the hope that it will be useful,
    #    but WITHOUT ANY WARRANTY; without even the implied warranty of
    #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    #    GNU Lesser General Public License for more details.
    #
    #    You should have received a copy of the GNU Lesser General Public
    #    License along with DEAP. If not, see <http://www.gnu.org/licenses/>.
    
    import random
    
    import numpy
    
    from deap import algorithms
    from deap import base
    from deap import creator
    from deap import tools
    
    # Problem parameter
    NB_QUEENS = 20
    INDIV_COUNT = 0
    
    
    def evalNQueens(individual, population):
        global INDIV_COUNT
        """Evaluation function for the n-queens problem.
        The problem is to determine a configuration of n queens
        on a nxn chessboard such that no queen can be taken by
        one another. In this version, each queens is assigned
        to one column, and only one queen can be on each line.
        The evaluation function therefore only counts the number
        of conflicts along the diagonals.
        """
        size = len(individual)
        # Count the number of conflicts with other queens.
        # The conflicts can only be diagonal, count on each diagonal line
        left_diagonal = [0] * (2 * size - 1)
        right_diagonal = [0] * (2 * size - 1)
    
        # Sum the number of queens on each diagonal:
        for i in range(size):
            left_diagonal[i + individual[i]] += 1
            right_diagonal[size - 1 - i + individual[i]] += 1
    
        # Count the number of conflicts on each diagonal
        sum_ = 0
        for i in range(2 * size - 1):
            if left_diagonal[i] > 1:
                sum_ += left_diagonal[i] - 1
            if right_diagonal[i] > 1:
                sum_ += right_diagonal[i] - 1
    
        if INDIV_COUNT % len(population) == 0:
            print(f'top 5 individuals @ generation {int(INDIV_COUNT / 300)}: {population[:5]}')
        INDIV_COUNT += 1
    
        return sum_,
    
    
    creator.create("FitnessMin", base.Fitness, weights=(-1.0,))
    creator.create("Individual", list, fitness=creator.FitnessMin)
    
    # Since there is only one queen per line,
    # individual are represented by a permutation
    toolbox = base.Toolbox()
    toolbox.register("permutation", random.sample, range(NB_QUEENS), NB_QUEENS)
    
    # Structure initializers
    # An individual is a list that represents the position of each queen.
    # Only the line is stored, the column is the index of the number in the list.
    toolbox.register("individual", tools.initIterate, creator.Individual, toolbox.permutation)
    toolbox.register("population", tools.initRepeat, list, toolbox.individual)
    
    toolbox.register("mate", tools.cxPartialyMatched)
    toolbox.register("mutate", tools.mutShuffleIndexes, indpb=2.0 / NB_QUEENS)
    toolbox.register("select", tools.selTournament, tournsize=3)
    
    
    def main(seed=0):
        random.seed(seed)
    
        pop = toolbox.population(n=300)
        toolbox.register("evaluate", evalNQueens, population=pop)
        hof = tools.HallOfFame(1)
        stats = tools.Statistics(lambda ind: ind.fitness.values)
        stats.register("Avg", numpy.mean)
        stats.register("Std", numpy.std)
        stats.register("Min", numpy.min)
        stats.register("Max", numpy.max)
    
        algorithms.eaSimple(pop, toolbox, cxpb=0.5, mutpb=0.2, ngen=100, stats=stats,
                            halloffame=hof, verbose=True)
    
        return pop, stats, hof
    
    
    if __name__ == "__main__":
        main()