From 2b1007a48a7fb02b7280a214a441be719def096d Mon Sep 17 00:00:00 2001 From: Hugo Aboud Date: Wed, 30 Dec 2020 07:05:58 -0300 Subject: [PATCH] nsga2 first commit --- .../{config-flightime => config-default} | 0 examples/hoverboard/config-nsga2 | 81 ++++ examples/hoverboard/evolve-flighdvnc.py | 164 ++++++++ examples/hoverboard/evolve-flightime.py | 35 +- examples/hoverboard/gui.py | 8 +- examples/hoverboard/sandbox.py | 13 + neat/__init__.py | 1 + neat/nsga2/__init__.py | 375 +++++++++++------- setup.py | 2 +- 9 files changed, 527 insertions(+), 152 deletions(-) rename examples/hoverboard/{config-flightime => config-default} (100%) create mode 100644 examples/hoverboard/config-nsga2 create mode 100644 examples/hoverboard/evolve-flighdvnc.py create mode 100644 examples/hoverboard/sandbox.py diff --git a/examples/hoverboard/config-flightime b/examples/hoverboard/config-default similarity index 100% rename from examples/hoverboard/config-flightime rename to examples/hoverboard/config-default diff --git a/examples/hoverboard/config-nsga2 b/examples/hoverboard/config-nsga2 new file mode 100644 index 00000000..e731e6ba --- /dev/null +++ b/examples/hoverboard/config-nsga2 @@ -0,0 +1,81 @@ +#--- parameters for the XOR-2 experiment ---# + +[NEAT] +fitness_criterion = max +fitness_threshold = 1000 +pop_size = 50 +reset_on_extinction = False + +[DefaultGenome] +# node activation options +activation_default = random +activation_mutate_rate = 0.5 +activation_options = relu gauss + +# node aggregation options +aggregation_default = random +aggregation_mutate_rate = 0.3 +aggregation_options = max sum + +# node bias options +bias_init_mean = 0.0 +bias_init_stdev = 1.0 +bias_max_value = 2.0 +bias_min_value = -2.0 +bias_mutate_power = 0.5 +bias_mutate_rate = 0.7 +bias_replace_rate = 0.1 + +# genome compatibility options +compatibility_disjoint_coefficient = 1.0 +compatibility_weight_coefficient = 0.5 + +# connection add/remove rates +conn_add_prob = 0.5 +conn_delete_prob = 0.5 + +# connection enable options +enabled_default = True +enabled_mutate_rate = 0.01 + +feed_forward = False +initial_connection = partial_nodirect 0.6 + +# node add/remove rates +node_add_prob = 0.4 +node_delete_prob = 0.4 + +# network parameters +num_hidden = 0 +num_inputs = 5 +num_outputs = 2 + +# node response options +response_init_mean = 1.0 +response_init_stdev = 0.0 +response_max_value = 30.0 +response_min_value = -30.0 +response_mutate_power = 0.0 +response_mutate_rate = 0.0 +response_replace_rate = 0.0 + +# connection weight options +weight_init_mean = 0.0 +weight_init_stdev = 60.0 +weight_max_value = 100 +weight_min_value = -100 +weight_mutate_power = 3 +weight_mutate_rate = 0.8 +weight_replace_rate = 0.3 + +[DefaultSpeciesSet] +compatibility_threshold = 20.0 + +[DefaultStagnation] +species_fitness_func = max +max_stagnation = 20 +species_elitism = 2 + +[NSGA2Reproduction] +elitism = 1 +survival_threshold = 0.2 diff --git a/examples/hoverboard/evolve-flighdvnc.py b/examples/hoverboard/evolve-flighdvnc.py new file mode 100644 index 00000000..f143237b --- /dev/null +++ b/examples/hoverboard/evolve-flighdvnc.py @@ -0,0 +1,164 @@ +""" + + Hoverboard: Flight Deviance (Double Fitness), using NSGA-II + + Small example tool to control the hoverboard game using NEAT. + It uses the NSGA2Reproduction method, with two fitness values: flight time and total distance from the center. + + # USAGE: + > python evolve-flightdvnc.py + > python evolve-flightdvnc.py --help + + @author: Hugo Aboud (@hugoaboud) + +""" + +from __future__ import print_function + +## DEBUG +## Uses local version of neat-python +import sys +sys.path.append('../../') +## DEBUG +import neat + +import os +import math +import argparse + +from hoverboard import Game +from visualize import watch + +# General Parameters + +GAME_TIME_STEP = 0.001 +CHECKPOINT_FOLDER = 'checkpoint-flightdvnc' +CONFIG_FILE = 'config-nsga2' + +# CLI Parameters + +GAME_START_ANGLE = 0 +SILENT = False +FAST_FORWARD = False + +# Evolution Flags + +BEST = None +GEN = 0 +POPULATION = None + +## +# Reporter +# Used to watch the game after each evaluation +## + +class GameReporter(neat.reporting.BaseReporter): + def post_evaluate(self, config, population, species, best_genome): + global BEST + global POPULATION + # If watch game is enabled (not silent) and best genome + # has changed, watch it + if (not SILENT and best_genome != BEST): + BEST = best_genome + species = POPULATION.species.get_species_id(BEST.key) + watch(config, GAME_TIME_STEP*(10 if FAST_FORWARD else 1), GEN, species, BEST, GAME_START_ANGLE) + +## +# Evaluation +## + +# Evaluate genome +def eval(genome, config): + # Create network + net = neat.nn.RecurrentNetwork.create(genome, config) + # Create game + game = Game(GAME_START_ANGLE,False) + # Run the game and calculate fitness (list) + genome.fitness = neat.nsga2.NSGA2Fitness(0,0) + while(True): + # Activate Neural Network + output = net.activate([game.hoverboard.velocity[0], game.hoverboard.velocity[1], game.hoverboard.ang_velocity, game.hoverboard.normal[0], game.hoverboard.normal[1]]) + + # Update game state from output and then update physics + game.hoverboard.set_thrust(output[0], output[1]) + game.update(GAME_TIME_STEP) + + # Fitness 0: flight time + # Fitness 1: distance from center (negative) + genome.fitness.add( GAME_TIME_STEP, + -math.sqrt((game.hoverboard.x-0.5)**2+(game.hoverboard.y-0.5)**2) ) + + # Fitness alternatives + #genome.fitness -= (game.hoverboard.normal[0]**2) + #genome.fitness -= math.sqrt(game.hoverboard.velocity[0]**2+game.hoverboard.velocity[1]**2) + #genome.fitness -= game.hoverboard.ang_velocity**2 + + # End of game + if (game.reset_flag): break + + genome.fitness.values[1] /= genome.fitness.values[0] + +# Evaluate generation +def eval_genomes(genomes, config): + # Global evolution flags + global GEN + # Evaluate each genome + for genome_id, genome in genomes: + eval(genome, config) + # NSGA-II required step: non-dominated sorting + POPULATION.reproduction.sort(genomes) + GEN += 1 + +## +# Main +## + +def main(): + + # Parse CLI arguments + parser = argparse.ArgumentParser(description='Example of evolving a Neural Network using neat-python to play a 2D hoverboard game.') + parser.add_argument('angle', help="Starting angle of the platform") + parser.add_argument('-c', '--checkpoint', help="Number of a checkpoint on the 'checkpoint-reference' folder to start from") + parser.add_argument('-s', '--silent', help="Don't watch the game", nargs='?', const=True, type=bool) + parser.add_argument('-f', '--fastfwd', help="Fast forward the game preview (10x)", nargs='?', const=True, type=bool) + args = parser.parse_args() + + # Store global parameters + global GAME_START_ANGLE + global SILENT + global FAST_FORWARD + GAME_START_ANGLE = float(args.angle) + SILENT = bool(args.silent) + FAST_FORWARD = bool(args.fastfwd) + + # Load neat configuration. + # Here's where we load the NSGA-II reproduction module + config = neat.Config(neat.DefaultGenome, neat.nsga2.NSGA2Reproduction, + neat.DefaultSpeciesSet, neat.DefaultStagnation, + CONFIG_FILE) + + # Create the population or load from checkpoint + global POPULATION + if (args.checkpoint != None): + POPULATION = neat.Checkpointer.restore_checkpoint(os.path.join(CHECKPOINT_FOLDER,'gen-'+str(args.checkpoint)), config) + else: + POPULATION = neat.Population(config) + + # Add a stdout reporter to show progress in the terminal. + POPULATION.add_reporter(neat.StdOutReporter(False)) + + # Add a game reporter to watch the game post evaluation + POPULATION.add_reporter(GameReporter()) + + # Add a checkpointer to save population pickles + if not os.path.exists(CHECKPOINT_FOLDER): + os.makedirs(CHECKPOINT_FOLDER) + POPULATION.add_reporter(neat.Checkpointer(1,filename_prefix=os.path.join(CHECKPOINT_FOLDER,'gen-'))) + + # Run until a solution is found. + winner = POPULATION.run(eval_genomes) + + # Display the winning genome. + print('\nBest genome:\n{!s}'.format(winner)) + +main() diff --git a/examples/hoverboard/evolve-flightime.py b/examples/hoverboard/evolve-flightime.py index aebb28c1..34d43780 100644 --- a/examples/hoverboard/evolve-flightime.py +++ b/examples/hoverboard/evolve-flightime.py @@ -1,6 +1,6 @@ """ - Hoverboard: Runtime (Single Fitness) + Hoverboard: Flight Time (Single Fitness) Small example tool to control the hoverboard game using NEAT. It uses the DefaultReproduction method, with a single fitness value: flight time. @@ -26,6 +26,7 @@ GAME_TIME_STEP = 0.001 CHECKPOINT_FOLDER = 'checkpoint-flightime' +CONFIG_FILE = 'config-default' # CLI Parameters @@ -39,6 +40,22 @@ GEN = 0 POPULATION = None +## +# Reporter +# Used to watch the game after each evaluation +## + +class GameReporter(neat.reporting.BaseReporter): + def post_evaluate(self, config, population, species, best_genome): + global BEST + global POPULATION + # If watch game is enabled (not silent) and best genome + # has changed, watch it + if (not SILENT and best_genome != BEST): + BEST = best_genome + species = POPULATION.species.get_species_id(BEST.key) + watch(config, GAME_TIME_STEP*(10 if FAST_FORWARD else 1), GEN, species, BEST, GAME_START_ANGLE) + ## # Evaluation ## @@ -74,21 +91,10 @@ def eval(genome, config): # Evaluate generation def eval_genomes(genomes, config): # Global evolution flags - global BEST global GEN - global POPULATION # Evaluate each genome looking for the best - max = None for genome_id, genome in genomes: eval(genome, config) - if (max == None or genome.fitness > max.fitness): - max = genome - # If watch game is enabled (not silent) and best genome - # has changed, watch it - if (not SILENT and max != BEST): - BEST = max - species = POPULATION.species.get_species_id(BEST.key) - watch(config, GAME_TIME_STEP*(10 if FAST_FORWARD else 1), GEN, species, BEST, GAME_START_ANGLE) GEN += 1 ## @@ -116,7 +122,7 @@ def main(): # Load neat configuration. config = neat.Config(neat.DefaultGenome, neat.DefaultReproduction, neat.DefaultSpeciesSet, neat.DefaultStagnation, - 'config-flightime') + CONFIG_FILE) # Create the population or load from checkpoint global POPULATION @@ -128,6 +134,9 @@ def main(): # Add a stdout reporter to show progress in the terminal. POPULATION.add_reporter(neat.StdOutReporter(False)) + # Add a game reporter to watch the game post evaluation + POPULATION.add_reporter(GameReporter()) + # Add a checkpointer to save population pickles if not os.path.exists(CHECKPOINT_FOLDER): os.makedirs(CHECKPOINT_FOLDER) diff --git a/examples/hoverboard/gui.py b/examples/hoverboard/gui.py index 0a6d9439..b35c7621 100644 --- a/examples/hoverboard/gui.py +++ b/examples/hoverboard/gui.py @@ -180,10 +180,10 @@ def render(self, screen): # if info enabled, render info texts if (self.info): img = self.font.render(str('GENERATION: {0}'.format(self.generation)), True, COLOR_TEXT) - screen.blit(img, (10,DISPLAY[1]-45)) + screen.blit(img, (DISPLAY[0]-70,DISPLAY[1]-45)) img = self.font.render(str('SPECIES: {0}'.format(self.species)), True, COLOR_TEXT) - screen.blit(img, (10,DISPLAY[1]-30)) + screen.blit(img, (DISPLAY[0]-70,DISPLAY[1]-30)) img = self.font.render(str('ID: {0}'.format(self.genome.key)), True, COLOR_TEXT) - screen.blit(img, (10,DISPLAY[1]-15)) + screen.blit(img, (DISPLAY[0]-70,DISPLAY[1]-15)) img = self.font.render(str('FITNESS: {0}'.format(self.genome.fitness)), True, COLOR_TEXT) - screen.blit(img, (DISPLAY[0]-120,DISPLAY[1]-15)) + screen.blit(img, (10,DISPLAY[1]-15)) diff --git a/examples/hoverboard/sandbox.py b/examples/hoverboard/sandbox.py new file mode 100644 index 00000000..9684e46b --- /dev/null +++ b/examples/hoverboard/sandbox.py @@ -0,0 +1,13 @@ +class Fitness: + def __init__(self, a, b): + self.a = a + self.b = b + def __str__(self): + return str((self.a,self.b)) + def __gt__(self, other): + return self.a > other.a + +fits = [Fitness(a,a*2) for a in range(10)] + +print (fits) +print (max(fits)) diff --git a/neat/__init__.py b/neat/__init__.py index 8f80630a..7954dec9 100644 --- a/neat/__init__.py +++ b/neat/__init__.py @@ -2,6 +2,7 @@ import neat.nn as nn import neat.ctrnn as ctrnn import neat.iznn as iznn +import neat.nsga2 as nsga2 import neat.distributed as distributed from neat.config import Config diff --git a/neat/nsga2/__init__.py b/neat/nsga2/__init__.py index 3645ef6f..bfc6b1b0 100644 --- a/neat/nsga2/__init__.py +++ b/neat/nsga2/__init__.py @@ -1,30 +1,127 @@ """ -Handles creation of genomes, either from scratch or by sexual or -asexual reproduction from parents. + + Implementation of NSGA-II as a reproduction method for NEAT. + + @autor: Hugo Aboud (@hugoaboud) + + # OVERVIEW + + NSGA-II is en Elitist Multiobjective Genetic Algorithm, designed to + efficiently sort populations based on multiple fitness values. + + The algorithm is proposed in two steps: + - 1: Fast Non-dominated Sorting + - 2: Crowding Distance Sorting + + Step 1 sorts the population in Parento-Front groups. + Step 2 creates a new population from the sorted old one + + # IMPLEMENTATION NOTES + + - In order to avoid unecessary changes to the neat-python library, a class + named NSGA2Fitness was created. It overloads the operators used by the lib, + keeping it concise with the definition. + - In order to comply with the single fitness progress/threshold, the first + fitness value is used for thresholding and when it's converted to a float + (like in mean methods). + - In order to use the multiobjective crowded-comparison operator, fitness + functions config should always be set to 'max'. + - Ranks are negative, so it's a maximization problem, as the default examples + + # IMPLEMENTATION + + - A NSGA2Fitness class is used to store multiple fitness values + during evaluation + - NSGA2Reproduction keeps track of parent population and species + - After all new genomes are evaluated, sort() method must be run + - sort() merges the current and parent population and sorts it + in parento-fronts, assigning a rank value to each + - When reproduce() is called by population, the default species + stagnation runs + - Then, Crowding Distance Sorting is used to remove the worst + genomes from the remaining species. + - The best genomes are stored as the parent population + - Each species then doubles in size by sexual/asexual reproduction + - TODO: If pop_size was not reached, cross genomes from different fronts + to incentivize innovation + """ from __future__ import division import math import random from itertools import count +from operator import add from neat.config import ConfigParameter, DefaultClassConfig from neat.math_util import mean -# TODO: Provide some sort of optional cross-species performance criteria, which -# are then used to control stagnation and possibly the mutation rate -# configuration. This scheme should be adaptive so that species do not evolve -# to become "cautious" and only make very slow progress. - +## +# NSGA-II Fitness +# Stores multiple fitness values +# Overloads operators allowing integration to unmodified neat-python +## + +class NSGA2Fitness: + def __init__(self, *values): + self.values = values + self.rank = 0 + self.dist = 0.0 + #self.score = 0.0 + def set(self, *values): + self.values = values + def add(self, *values): + self.values = list(map(add, self.values, values)) + + def dominates(self, other): + d = False + for a, b in zip(self.values, other.values): + if (a < b): return False + elif (a > b): d = True + return d + + # > + def __gt__(self, other): + # comparison of fitnesses on tournament, use crowded-comparison operator + # this is also used by max/min + if (isinstance(other,NSGA2Fitness)): + if (self.rank > other.rank): return True + elif (self.rank == other.rank and self.dist > other.dist): return True + return False + # stagnation.py initializes fitness as -sys.float_info.max + # it's the only place where the next line should be called + return self.rank > other + # >= + def __ge__(self, other): + # population.run() compares fitness to the fitness threshold for termination + # it's the only place where the next line should be called + # it's also the only place where score participates of evolution + # besides that, score is a value for reporting the general evolution + return self.values[0] >= other + # - + def __sub__(self, other): + # used only by reporting->neat.math_util to calculate fitness (score) variance + #return self.score - other + return self.values[0] - other + # float() + def __float__(self): + # used only by reporting->neat.math_util to calculate mean fitness (score) + #return self.score + return float(self.values[0]) + # str() + def __str__(self): + #return "rank:{0},score:{1},values:{2}".format(self.rank, self.score, self.values) + return "rank:{0},dist:{1},values:{2}".format(self.rank, self.dist, self.values) + +## +# NSGA-II Reproduction +# Implements "Non-Dominated Sorting" and "Crowding Distance Sorting" to reproduce the population +## class NSGA2Reproduction(DefaultClassConfig): - """ - Implements the default NEAT-python reproduction scheme: - explicit fitness sharing with fixed-time species stagnation. - """ - @classmethod def parse_config(cls, param_dict): + return DefaultClassConfig(param_dict, [ConfigParameter('elitism', int, 0), ConfigParameter('survival_threshold', float, 0.2), @@ -36,8 +133,17 @@ def __init__(self, config, reporters, stagnation): self.reporters = reporters self.genome_indexer = count(1) self.stagnation = stagnation - self.ancestors = {} + # Parent population and species + # This population is mixed with the evaluated population in order to achieve elitism + self.parent_pop = [] + self.parent_species = {} + + # Parento-fronts of genomes (including population and parent population) + # These are created by the sort() method at the end of the fitness evaluation process + self.fronts = [] + + # new population, called by the population constructor def create_new(self, genome_type, genome_config, num_genomes): new_genomes = {} for i in range(num_genomes): @@ -45,144 +151,145 @@ def create_new(self, genome_type, genome_config, num_genomes): g = genome_type(key) g.configure_new(genome_config) new_genomes[key] = g - self.ancestors[key] = tuple() - return new_genomes - @staticmethod - def compute_spawn(adjusted_fitness, previous_sizes, pop_size, min_species_size): - """Compute the proper number of offspring per species (proportional to fitness).""" - af_sum = sum(adjusted_fitness) - - spawn_amounts = [] - for af, ps in zip(adjusted_fitness, previous_sizes): - if af_sum > 0: - s = max(min_species_size, af / af_sum * pop_size) - else: - s = min_species_size - - d = (s - ps) * 0.5 - c = int(round(d)) - spawn = ps - if abs(c) > 0: - spawn += c - elif d > 0: - spawn += 1 - elif d < 0: - spawn -= 1 - - spawn_amounts.append(spawn) + # NSGA-II step 1: fast non-dominated sorting + # This >must< be called by the fitness function (aka eval_genomes) + # after a NSGA2Fitness was assigned to each genome + def sort(self, genomes): + print("NSGA-II step 1: non-dominated sorting") + genomes = [g[1] for g in genomes] + self.parent_pop + # algorithm data + S = {} # genomes dominated by key genome + n = {} # counter of genomes dominating key genome + F = [] # current dominance front + self.fronts = [] # clear dominance fronts + # calculate dominance of every genome to every other genome - O(MN²) + for p in range(len(genomes)): + S[p] = [] + n[p] = 0 + for q in range(len(genomes)): + if (p == q): continue + # p dominates q + if (genomes[p].fitness.dominates(genomes[q].fitness)): + S[p].append(q) + # q dominates p + elif (genomes[q].fitness.dominates(genomes[p].fitness)): + n[p] += 1 + # if genome is non-dominated, set rank and add to front + if (n[p] == 0): + genomes[p].fitness.rank = 0 + F.append(p) + + # assemble dominance fronts - O(N²) + i = 0 # dominance front iterator + while (len(F) > 0): + # store front + self.fronts.append([genomes[f] for f in F]) + # new dominance front + Q = [] + # for each genome in current front + for p in F: + # for each genome q dominated by p + for q in S[p]: + # decrease dominate counter of q + n[q] -= 1 + # if q reached new front + if n[q] == 0: + genomes[q].fitness.rank = -(i+1) + Q.append(q) + # iterate front + i += 1 + F = Q + + # NSGA-II step 2: crowding distance sorting + # this is where NSGA-2 reproduces the population by the fitness rank + # calculated on step 1 + def reproduce(self, config, species, pop_size, generation): - # Normalize the spawn amounts so that the next generation is roughly - # the population size requested by the user. - total_spawn = sum(spawn_amounts) - norm = pop_size / total_spawn - spawn_amounts = [max(min_species_size, int(round(n * norm))) for n in spawn_amounts] + # Disclaimer: this method uses no absolute fitness values + # The fitnesses are compared through the crowded-comparison operator + # fitness.values[0] is used for fitness threshold and reporting, but not in here + print("NSGA-II step 2: crowding distance sorting") - return spawn_amounts + # append parent species to list, so all front genomes are covered + species.species.update(self.parent_species) - def reproduce(self, config, species, pop_size, generation): - """ - Handles creation of genomes, either from scratch or by sexual or - asexual reproduction from parents. - """ - # TODO: I don't like this modification of the species and stagnation objects, - # because it requires internal knowledge of the objects. - - # Filter out stagnated species, collect the set of non-stagnated - # species members, and compute their average adjusted fitness. - # The average adjusted fitness scheme (normalized to the interval - # [0, 1]) allows the use of negative fitness values without - # interfering with the shared fitness scheme. - all_fitnesses = [] - remaining_species = [] + # Default Stagnation without fitness calculation + # Filter out stagnated species genomes, collect the set of non-stagnated + remaining_species = {} for stag_sid, stag_s, stagnant in self.stagnation.update(species, generation): if stagnant: self.reporters.species_stagnant(stag_sid, stag_s) else: - all_fitnesses.extend(m.fitness for m in stag_s.members.values()) - remaining_species.append(stag_s) - # The above comment was not quite what was happening - now getting fitnesses - # only from members of non-stagnated species. + remaining_species[stag_sid] = stag_s - # No species left. + # No genomes left. if not remaining_species: species.species = {} - return {} # was [] - - # Find minimum/maximum fitness across the entire population, for use in - # species adjusted fitness computation. - min_fitness = min(all_fitnesses) - max_fitness = max(all_fitnesses) - # Do not allow the fitness range to be zero, as we divide by it below. - # TODO: The ``1.0`` below is rather arbitrary, and should be configurable. - fitness_range = max(1.0, max_fitness - min_fitness) - for afs in remaining_species: - # Compute adjusted fitness. - msf = mean([m.fitness for m in afs.members.values()]) - af = (msf - min_fitness) / fitness_range - afs.adjusted_fitness = af - - adjusted_fitnesses = [s.adjusted_fitness for s in remaining_species] - avg_adjusted_fitness = mean(adjusted_fitnesses) # type: float - self.reporters.info("Average adjusted fitness: {:.3f}".format(avg_adjusted_fitness)) - - # Compute the number of new members for each species in the new generation. - previous_sizes = [len(s.members) for s in remaining_species] - min_species_size = self.reproduction_config.min_species_size - # Isn't the effective min_species_size going to be max(min_species_size, - # self.reproduction_config.elitism)? That would probably produce more accurate tracking - # of population sizes and relative fitnesses... doing. TODO: document. - min_species_size = max(min_species_size, self.reproduction_config.elitism) - spawn_amounts = self.compute_spawn(adjusted_fitnesses, previous_sizes, - pop_size, min_species_size) + return {} + + # Crowding distance assignment + # Create new parent population from the best fronts + self.parent_pop = [] + for front in self.fronts: + + ## WIP: Calculate crowd-distance + for genome in front: + genome.dist = 0 + fitnesses = [f.fitness for f in front] + for m in range(len(fitnesses[0].values)): + fitnesses.sort(key=lambda f: f.values[m]) + scale = (fitnesses[-1].values[m]-fitnesses[0].values[m]) + fitnesses[0].dist = float('inf') + fitnesses[-1].dist = float('inf') + if (scale > 0): + for i in range(1,len(fitnesses)-1): + fitnesses[i].dist += abs(fitnesses[i+1].values[0]-fitnesses[i-1].values[0])/scale + + # front fits entirely on the parent population, just append it + if (len(self.parent_pop) + len(front) < pop_size): + self.parent_pop += front + # front exceeds parent population, sort by crowd distance and + # append only what's necessary to reach pop_size + else: + front.sort(key=lambda g: g.fitness) + self.parent_pop += front[:pop_size-len(self.parent_pop)] + # Map parent species, by removing the genomes from remaining_species + # that haven't passed the crowding-distance step + self.parent_species = remaining_species + for _, sp in remaining_species.items(): + # filter genomes from each species + sp.member = [g for g in sp.members if g in self.parent_pop] + + # Remove empty species + self.parent_species = {id:sp for id,sp in self.parent_species.items() if len(sp.members) > 0} + + # Reproduce species of parent population into new population + # Each species doubles in size new_population = {} - species.species = {} - for spawn, s in zip(spawn_amounts, remaining_species): - # If elitism is enabled, each species always at least gets to retain its elites. - spawn = max(spawn, self.reproduction_config.elitism) - - assert spawn > 0 - - # The species has at least one member for the next generation, so retain it. - old_members = list(s.members.items()) - s.members = {} - species.species[s.key] = s - - # Sort members in order of descending fitness. - old_members.sort(reverse=True, key=lambda x: x[1].fitness) - - # Transfer elites to new generation. - if self.reproduction_config.elitism > 0: - for i, m in old_members[:self.reproduction_config.elitism]: - new_population[i] = m - spawn -= 1 - - if spawn <= 0: - continue - - # Only use the survival threshold fraction to use as parents for the next generation. - repro_cutoff = int(math.ceil(self.reproduction_config.survival_threshold * - len(old_members))) - # Use at least two parents no matter what the threshold fraction result is. - repro_cutoff = max(repro_cutoff, 2) - old_members = old_members[:repro_cutoff] - - # Randomly choose parents and produce the number of offspring allotted to the species. - while spawn > 0: - spawn -= 1 - - parent1_id, parent1 = random.choice(old_members) - parent2_id, parent2 = random.choice(old_members) - - # Note that if the parents are not distinct, crossover will produce a - # genetically identical clone of the parent (but with a different ID). + for _, species in self.parent_species.items(): + # spawn the number of members on the species + spawn = len(species.members) + # special case: single member, asexual reproduction + if (spawn == 1): + parent = [g for _, g in species.members.items()][0] gid = next(self.genome_indexer) child = config.genome_type(gid) - child.configure_crossover(parent1, parent2, config.genome_config) + child.configure_crossover(parent, parent, config.genome_config) child.mutate(config.genome_config) new_population[gid] = child - self.ancestors[gid] = (parent1_id, parent2_id) + # usual case: n > 1 members, sexual reproduction + else: + for i in range(spawn): + # pick two random parents + parents = random.sample(list(species.members.values()), 2) + # sexual reproduction + gid = next(self.genome_indexer) + child = config.genome_type(gid) + child.configure_crossover(parents[0], parents[1], config.genome_config) + child.mutate(config.genome_config) + new_population[gid] = child return new_population diff --git a/setup.py b/setup.py index c9870225..c91037a2 100644 --- a/setup.py +++ b/setup.py @@ -12,7 +12,7 @@ description='A NEAT (NeuroEvolution of Augmenting Topologies) implementation', long_description='Python implementation of NEAT (NeuroEvolution of Augmenting Topologies), a method ' + 'developed by Kenneth O. Stanley for evolving arbitrary neural networks.', - packages=['neat', 'neat/iznn', 'neat/nn', 'neat/ctrnn'], + packages=['neat', 'neat/iznn', 'neat/nn', 'neat/ctrnn', 'neat/nsga2'], classifiers=[ 'Development Status :: 4 - Beta', 'Intended Audience :: Developers',