Source code for zellij.strategies.genetic_algorithm

# @Author: Thomas Firmin <ThomasFirmin>
# @Date:   2022-05-03T15:41:48+02:00
# @Email:  thomas.firmin@univ-lille.fr
# @Project: Zellij
# @Last modified by:   tfirmin
# @Last modified time: 2022-10-03T22:38:18+02:00
# @License: CeCILL-C (http://www.cecill.info/index.fr.html)


from zellij.core.metaheuristic import Metaheuristic
from zellij.core.addons import Mutator, Selector, Crossover
from deap import base
from deap import creator
from deap import tools
import numpy as np
import os
import pandas as pd

import logging

logger = logging.getLogger("zellij.GA")


[docs]class Genetic_algorithm(Metaheuristic): """Genetic_algorithm Genetic_algorithm (GA) implements a classic genetic algorithm. It uses `DEAP <https://deap.readthedocs.io/>`__. See :ref:`meta` for more info. Attributes ---------- pop_size : int Population size of the GA.\ In a distributed environment (e.g. MPILoss), it has an influence on the parallelization quality.\ It must be tuned according the available hardware. generation : int Generation number of the GA. elitism : float, default=0.5 Percentage of the best parents to keep in the next population by replacing the worst children. Default 50%. filename : str, optional If a file containing initial solutions. GA will initialize the population with it. See Also -------- :ref:`meta` : Parent class defining what a Metaheuristic is in Zellij. :ref:`lf` : Describes what a loss function is in Zellij. :ref:`sp` : Describes what a search space is in Zellij. Examples -------- >>> from zellij.core import Loss >>> from zellij.core import ContinuousSearchspace >>> from zellij.core import FloatVar, ArrayVar >>> from zellij.utils.neighborhoods import FloatInterval, ArrayInterval, Intervals >>> from zellij.strategies.genetic_algorithm import Genetic_algorithm >>> from zellij.utils.operators import NeighborMutation, DeapTournament, DeapOnePoint >>> from zellij.utils.benchmark import himmelblau ... >>> lf = Loss()(himmelblau) >>> sp = ContinuousSearchspace(ArrayVar( ... FloatVar("a",-5,5, neighbor=FloatInterval(0.5)), ... FloatVar("b",-5,5,neighbor=FloatInterval(0.5)), ... neighbor=ArrayInterval()) ... ,lf, neighbor=Intervals(), ... mutation = NeighborMutation(0.5), ... selection = DeapTournament(3), ... crossover = DeapOnePoint()) ... >>> ga = Genetic_algorithm(sp, 1000, pop_size=25, generation=40,elitism=0.5) >>> ga.run() """ def __init__( self, search_space, f_calls, pop_size=10, generation=1000, elitism=0.5, filename="", verbose=True, ): """__init__(search_space, f_calls, pop_size = 10, generation = 1000, verbose=True) Initialize Genetic_algorithm class Parameters ---------- search_space : Searchspace Search space object containing bounds of the search space. f_calls : int Maximum number of loss_func calls pop_size : int Population size of the GA.\ generation : int Generation number of the GA. elitism : float Percentage of the best parents to keep in the next population by replacing the worst children. filename : str, optional If a file containing initial solutions. GA will initialize the population with it. verbose : boolean, default=True Algorithm verbosity """ super().__init__(search_space, f_calls, verbose) assert hasattr(search_space, "mutation") and isinstance( search_space.mutation, Mutator ), f"""When using :ref:`ga`, :ref:`sp` must have a `mutation` operator and of type: Mutator, use `mutation` kwarg when defining the :ref:`sp` ex:\n >>> ContinuousSearchspace(values, loss, mutation=...)""" assert hasattr(search_space, "selection") and isinstance( search_space.selection, Selector ), f"""When using :ref:`ga`, :ref:`sp` must have a `selection` operator and of type: Selector, use `mutation` kwarg when defining the :ref:`sp` ex:\n >>> ContinuousSearchspace(values, loss, selection=...)""" assert hasattr(search_space, "crossover") and isinstance( search_space.crossover, Crossover ), f"""When using :ref:`ga`, :ref:`sp` must have a `mutation` operator and of type: Selector, use `crossover` kwarg when defining the :ref:`sp` ex:\n >>> ContinuousSearchspace(values, loss, crossover=...)""" self.pop_size = pop_size self.generation = generation self.elitism = elitism self.pop_historic = [] self.fitness_historic = [] # Population save self.filename = filename self.ga_save = "" # Define what an individual is
[docs] def define_individual(self, sp): """define_individual(self) Describe how an individual should be initialized. Here a random point from SearchSpace is sampled. """ # Select one random point from the search space solution = sp.random_point() return solution
# Initialize an individual extracted from a file
[docs] def initIndividual(self, icls, content): """initIndividual(self, icls, content) Initialize an individual to DEAP. """ return icls([content.to_list()])
# Initialize a population extracted from a file
[docs] def initPopulation(self, pcls, ind_init, filename): """initPopulation(self, pcls, ind_init, filename) Initialize a population of individual, from a file, to DEAP. """ data = pd.read_csv( filename, sep=",", decimal=".", usecols=self.search_space.size, ) contents = data.tail(pop_size) return pcls(ind_init(c) for index, c in contents.iterrows())
# Run GA
[docs] def run(self, H=None, n_process=1): """run(H=None, n_process=1) Runs GA Parameters ---------- H : Fractal, optional When used by :ref:`dba`, a fractal corresponding to the current subspace is given n_process : int, default=1 Determine the number of best solution found to return. Returns ------- best_sol : list[float] Returns a list of the :code:`n_process` best found points to the continuous format best_scores : list[float] Returns a list of the :code:`n_process` best found scores associated to best_sol """ # Progress bar self.build_bar(self.generation) self.search_space.loss.file_created = False logger.info("Starting") logger.info("Constructing tools...") # Define problem type "fitness", weights = -1.0 -> minimization problem creator.create("FitnessMin", base.Fitness, weights=(-1.0,)) # Define what an individual is for the algorithm creator.create("Individual", list, fitness=creator.FitnessMin) # Toolbox contains all the operator of GA. (mutate, select, crossover...) toolbox = base.Toolbox() # Start from a saved population if self.filename: toolbox.register( "individual_guess", self.initIndividual, creator.Individual ) toolbox.register( "population_guess", self.initPopulation, list, toolbox.individual_guess, self.filename, ) logger.info("Creation of the initial population...") pop = toolbox.population_guess() # Start from a random population else: if H: sp = H else: sp = self.search_space # Determine what an individual is toolbox.register("hyperparameters", self.define_individual, sp) # Determine the way to build individuals for the population toolbox.register( "individual", tools.initRepeat, creator.Individual, toolbox.hyperparameters, n=1, ) # Determine the way to build a population toolbox.register( "population", tools.initRepeat, list, toolbox.individual ) logger.info("Creation of the initial population...") # Build the population pop = toolbox.population(n=self.pop_size) # Create operators self.search_space.mutation._build(toolbox) self.search_space.selection._build(toolbox) self.search_space.crossover._build(toolbox) # Create a tool to select best individuals from a population bpn = int(self.pop_size * self.elitism) bcn = self.pop_size - bpn toolbox.register("best_p", tools.selBest, k=bpn) toolbox.register("best_c", tools.selBest, k=bcn) best_of_all = tools.HallOfFame(n_process) # Ga initialization logger.info("Evaluating the initial population...") # Compute dynamically fitnesses solutions = [] solutions = [p[0] for p in pop] # Progress bar self.pending_pb(len(solutions)) fitnesses = self.search_space.loss(solutions, generation=0) self.update_main_pb( len(solutions), explor=True, best=self.search_space.loss.new_best ) # Map computed fitness to individual fitness value for ind, fit in zip(pop, fitnesses): ind.fitness.values = (fit,) fits = [ind.fitness.values[0] for ind in pop] for ind, cout in zip(pop, fits): self.pop_historic.append(ind[0]) self.fitness_historic.append(cout) logger.info("Initial population evaluated") logger.info("Evolution starting...") g = 0 while ( g < self.generation and self.search_space.loss.calls < self.f_calls ): g += 1 # Progress bar self.meta_pb.update() # Update all of fame best_of_all.update(pop) logger.debug("Generation: " + str(g)) # Selection operator logger.debug("Selection...") offspring = self.search_space.selection(pop, k=len(pop)) children = [] # Crossover operator logger.debug("Crossover...") i = 0 for child1, child2 in zip(offspring[::2], offspring[1::2]): # Clone individuals from crossover children1 = toolbox.clone(child1) children2 = toolbox.clone(child2) # Apply crossover self.search_space.crossover(children1[0], children2[0]) # Delete children fitness inherited from the parents del children1.fitness.values del children2.fitness.values # Add new children to list children.append(children1) children.append(children2) # Mutate children logger.debug("Mutation...") for mutant in children: toolbox.mutate(mutant[0]) logger.debug("Evaluating population n°" + str(g)) # Compute dynamically fitnesses solutions = [] solutions = [p[0] for p in children] # progress bar self.pending_pb(len(solutions)) fitnesses = self.search_space.loss(solutions, generation=g) # Progress bar self.update_main_pb( len(solutions), explor=True, best=self.search_space.loss.new_best, ) # Map computed fitness to individual fitness value for ind, fit in zip(children, fitnesses): ind.fitness.values = (fit,) # Build new population pop[:] = toolbox.best_p(offspring) + toolbox.best_c(children) # Get fitnesses from the new population fits = [ind.fitness.values[0] for ind in pop] for ind, cout in zip(pop, fits): self.pop_historic.append(ind[0]) self.fitness_historic.append(cout) # End population evaluation logger.info(f"Evaluation n°{g} ending...") best = [] min = [] logger.info("Ending") for b in best_of_all: min.append(b.fitness.values[0]) best.append(b[0]) # print best parameters from genetic algorithm logger.info( "Best parameters: " + str(b[0]) + " | score: " + str(b.fitness.values[0]) ) self.close_bar() return best, min