def __init__(self, config, initial_population=None): """ :param config: Either a config.Config object or path to a configuration file. :param initial_population: """ # If config is not a Config object, assume it is a path to the config file. if not isinstance(config, Config): config = Config(config) self.config = config self.diversity = config.diversity_type(self.config) self.species_indexer = Indexer(1) self.genome_indexer = Indexer(1) self.species = [] self.generation_statistics = [] self.most_fit_genomes = [] self.generation = -1 self.total_evaluations = 0 if initial_population is None: initial_population = self._create_population() # Partition the population into species based on current configuration. self._speciate(initial_population)
def __init__(self, config, reporters, stagnation): self.elitism = int(config.get('elitism')) self.survival_threshold = float(config.get('survival_threshold')) self.reporters = reporters self.genome_indexer = Indexer(1) self.stagnation = stagnation self.ancestors = {}
def get_new_node_key(self, node_dict): if not hasattr(self, 'node_indexer'): self.node_indexer = Indexer(max(list(iterkeys(node_dict))) + 1) new_id = self.node_indexer.get_next() assert new_id not in node_dict return new_id
def __init__(self, config, reporters): self.config = config params = config.get_type_config(self) self.elitism = int(params.get('elitism')) self.survival_threshold = float(params.get('survival_threshold')) self.reporters = reporters self.genome_indexer = Indexer(1) self.stagnation = config.stagnation_type(config, reporters)
class Neuron(object): """ A simple sigmoidal neuron """ # TODO: Get rid of these global indexers. indexer = Indexer(0) def __init__(self, neuron_type, ID, bias, response, activation_type): assert neuron_type in ('INPUT', 'OUTPUT', 'HIDDEN') self.type = neuron_type self.ID = self.indexer.next(ID) self.bias = bias self.response = response self.activation = activation_functions.get(activation_type) self._synapses = [] self.output = 0.0 # for recurrent networks all neurons must have an "initial state" def activate(self): """Activates the neuron""" assert self.type is not 'INPUT' z = self.bias + self.response * self._update_activation() return self.activation(z) def _update_activation(self): soma = 0.0 for s in self._synapses: soma += s.incoming() return soma def create_synapse(self, s): self._synapses.append(s) def __repr__(self): return '{0:d} {1!s}'.format(self.ID, self.type)
def test_indexer(): indexer0 = Indexer(0) assert indexer0.get_next() == 0 assert indexer0.get_next() == 1 assert indexer0.get_next() == 2 indexer17 = Indexer(17) assert indexer17.get_next() == 17 assert indexer17.get_next() == 18 assert indexer17.get_next() == 19
def __init__(self, config, initial_population=None): """ :param config: Either a config.Config object or path to a configuration file. :param initial_population: Either an initial set of Genome instances to be used as the initial population, or None, in which case a randomized set of Genomes will be created automatically based on the configuration parameters. """ # If config is not a Config object, assume it is a path to the config file. if not isinstance(config, Config): config = Config(config) # Configure statistics and reporting as requested by the user. self.reporters = ReporterSet() if config.collect_statistics: self.statistics = StatisticsReporter() self.add_reporter(self.statistics) else: self.statistics = None if config.report: self.add_reporter(StdOutReporter()) self.config = config self.species_indexer = Indexer(1) self.genome_indexer = Indexer(1) self.innovation_indexer = InnovationIndexer(0) self.reproduction = config.reproduction_type(self.config, self.reporters, self.genome_indexer, self.innovation_indexer) self.species = [] self.generation = -1 self.total_evaluations = 0 # Create a population if one is not given, then partition into species. if initial_population is None: initial_population = self._create_population() self._speciate(initial_population)
def __init__(self, config, initial_population=None): """ :param config: Either a config.Config object or path to a configuration file. :param initial_population: Either an initial set of Genome instances to be used as the initial population, or None, in which case a randomized set of Genomes will be created automatically based on the configuration parameters. """ # If config is not a Config object, assume it is a path to the config file. if not isinstance(config, Config): config = Config(config) # Configure statistics and reporting as requested by the user. self.reporters = ReporterSet() if config.collect_statistics: self.statistics = StatisticsReporter() self.add_reporter(self.statistics) else: self.statistics = None if config.report: self.add_reporter(StdOutReporter()) self.config = config self.species_indexer = Indexer(1) self.genome_indexer = Indexer(1) self.innovation_indexer = InnovationIndexer(0) self.reproduction = config.reproduction_type( self.config, self.reporters, self.genome_indexer, self.innovation_indexer ) self.species = [] self.generation = -1 self.total_evaluations = 0 # Create a population if one is not given, then partition into species. if initial_population is None: initial_population = self._create_population() self._speciate(initial_population)
def test_indexer(): indexer = Indexer(0) assert indexer.get_next() == 0 assert indexer.get_next() == 1 # TODO: Why doesn't Indexer remember its starting value given in the ctor? indexer.clear() assert indexer.get_next() == 1 assert indexer.get_next() == 2
class SpeciesSet(object): """ Encapsulates the speciation scheme. """ def __init__(self, config): self.config = config self.indexer = Indexer(1) self.species = [] def speciate(self, population): """ Place genomes into species by genetic similarity. Note that this method assumes the current representatives of the species are from the old generation, and that after speciation has been performed, the old representatives should be dropped and replaced with representatives from the new generation. If you violate this assumption, you should make sure other necessary parts of the code are updated to reflect the new behavior. """ for individual in population: # Find the species with the most similar representative. min_distance = None closest_species = None for s in self.species: distance = individual.distance(s.representative) if distance < self.config.compatibility_threshold \ and (min_distance is None or distance < min_distance): closest_species = s min_distance = distance if closest_species is not None: closest_species.add(individual) else: # No species is similar enough, create a new species for this individual. self.species.append(Species(individual, self.indexer.get_next())) # Only keep non-empty species. self.species = [s for s in self.species if s.members] # Select a random current member as the new representative. for s in self.species: s.representative = random.choice(s.members)
class Population(object): """ This class implements the core NEAT algorithm. It maintains a list of Species instances, each of which contains a collection of Genome instances. """ def __init__(self, config, initial_population=None): """ :param config: Either a config.Config object or path to a configuration file. :param initial_population: Either an initial set of Genome instances to be used as the initial population, or None, in which case a randomized set of Genomes will be created automatically based on the configuration parameters. """ # If config is not a Config object, assume it is a path to the config file. if not isinstance(config, Config): config = Config(config) # Configure statistics and reporting as requested by the user. self.reporters = ReporterSet() if config.collect_statistics: self.statistics = StatisticsReporter() self.add_reporter(self.statistics) else: self.statistics = None if config.report: self.add_reporter(StdOutReporter()) self.config = config self.species_indexer = Indexer(1) self.genome_indexer = Indexer(1) self.innovation_indexer = InnovationIndexer(0) self.reproduction = config.reproduction_type( self.config, self.reporters, self.genome_indexer, self.innovation_indexer ) self.species = [] self.generation = -1 self.total_evaluations = 0 # Create a population if one is not given, then partition into species. if initial_population is None: initial_population = self._create_population() self._speciate(initial_population) def add_reporter(self, reporter): self.reporters.add(reporter) def remove_reporter(self, reporter): self.reporters.remove(reporter) def load_checkpoint(self, filename): """Resumes the simulation from a previous saved point.""" self.reporters.loading_checkpoint(filename) with gzip.open(filename) as f: (self.species, self.generation, random_state) = pickle.load(f) random.setstate(random_state) def save_checkpoint(self, filename=None, checkpoint_type="user"): """ Save the current simulation state. """ if filename is None: filename = "neat-checkpoint-{0}".format(self.generation) self.reporters.saving_checkpoint(checkpoint_type, filename) with gzip.open(filename, "w", compresslevel=5) as f: data = (self.species, self.generation, random.getstate()) pickle.dump(data, f, protocol=pickle.HIGHEST_PROTOCOL) def _create_population(self): # Create a collection of unconnected genomes with no hidden nodes. new_population = [] # TODO: The genotype class should know how to do everything below, based # solely on what's in the config object. This allows users to completely # replace the initial population creation scheme if they choose. for i in range(self.config.pop_size): g_id = self.genome_indexer.next() g = self.config.genotype.create_unconnected(g_id, self.config) new_population.append(g) # Add hidden nodes if requested. if self.config.hidden_nodes > 0: for g in new_population: g.add_hidden_nodes(self.config.hidden_nodes) # Add connections based on initial connectivity type. if self.config.initial_connection == "fs_neat": for g in new_population: g.connect_fs_neat(self.innovation_indexer) elif self.config.initial_connection == "fully_connected": for g in new_population: g.connect_full(self.innovation_indexer) elif self.config.initial_connection == "partial": for g in new_population: g.connect_partial(self.innovation_indexer, self.config.connection_fraction) return new_population def _speciate(self, population): """ Place genomes into species by genetic similarity. Note that this method assumes the current representatives of the species are from the old generation, and that after speciation has been performed, the old representatives should be dropped and replaced with representatives from the new generation. If you violate this assumption, you should make sure other necessary parts of the code are updated to reflect the new behavior. """ for individual in population: # Find the species with the most similar representative. min_distance = None closest_species = None for s in self.species: distance = individual.distance(s.representative) if distance < self.config.compatibility_threshold: if min_distance is None or distance < min_distance: closest_species = s min_distance = distance if closest_species: closest_species.add(individual) else: # No species is similar enough, create a new species for this individual. self.species.append(Species(individual, self.species_indexer.next())) # Only keep non-empty species. self.species = [s for s in self.species if s.members] # Select a random current member as the new representative. for s in self.species: s.representative = random.choice(s.members) def run(self, fitness_function, n): """ Runs NEAT's genetic algorithm for n generations. The user-provided fitness_function should take one argument, a list of all genomes in the population, and its return value is ignored. This function is free to maintain external state, perform evaluations in parallel, and probably any other thing you want. The only requirement is that each individual's fitness member must be set to a floating point value after this function returns. It is assumed that fitness_function does not modify the list of genomes, or the genomes themselves, apart from updating the fitness member. """ # Remember start time for saving timed checkpoints. last_checkpoint = time.time() for g in range(n): self.generation += 1 self.reporters.start_generation(self.generation) # Collect a list of all members from all species. population = [] for s in self.species: population.extend(s.members) # Evaluate all individuals in the population using the user-provided function. # TODO: Add an option to only evaluate each genome once, to reduce number of # fitness evaluations in cases where the fitness is known to be the same if the # genome doesn't change--in these cases, evaluating unmodified elites in each # generation is a waste of time. fitness_function(population) self.total_evaluations += len(population) # Gather and report statistics. best = max(population) self.reporters.post_evaluate(population, self.species, best) # Save the best genome from the current generation if requested. if self.config.save_best: with open("best_genome_" + str(self.generation), "wb") as f: pickle.dump(best, f) # End if the fitness threshold is reached. if best.fitness >= self.config.max_fitness_threshold: self.reporters.found_solution(self.generation, best) break # Create the next generation from the current generation. self.species, new_population = self.reproduction.reproduce(self.species, self.config.pop_size) # Check for complete extinction if not self.species: self.reporters.complete_extinction() # If requested by the user, create a completely new population, # otherwise raise an exception. if self.config.reset_on_extinction: new_population = self._create_population() else: raise CompleteExtinctionException() # Update species age. for s in self.species: s.age += 1 # Divide the new population into species. self._speciate(new_population) # Save checkpoints if necessary. if self.config.checkpoint_time_interval is not None: timed_checkpoint_due = last_checkpoint + 60 * self.config.checkpoint_time_interval if time.time() >= timed_checkpoint_due: self.save_checkpoint(checkpoint_type="timed") last_checkpoint = time.time() if ( self.config.checkpoint_gen_interval is not None and self.generation % self.config.checkpoint_gen_interval == 0 ): self.save_checkpoint(checkpoint_type="generation") self.reporters.end_generation()
class DefaultReproduction(object): """ Handles creation of genomes, either from scratch or by sexual or asexual reproduction from parents. Implements the default NEAT-python reproduction scheme: explicit fitness sharing with fixed-time species stagnation. """ # TODO: Create a separate configuration class instead of using a dict (for consistency with other types). @classmethod def parse_config(cls, param_dict): config = {'elitism': 1, 'survival_threshold': 0.2} config.update(param_dict) return config @classmethod def write_config(cls, f, param_dict): elitism = param_dict.get('elitism', 1) f.write('elitism = {}\n'.format(elitism)) survival_threshold = param_dict.get('survival_threshold', 0.2) f.write('survival_threshold = {}\n'.format(survival_threshold)) def __init__(self, config, reporters, stagnation): self.elitism = int(config.get('elitism')) self.survival_threshold = float(config.get('survival_threshold')) self.reporters = reporters self.genome_indexer = Indexer(1) self.stagnation = stagnation self.ancestors = {} def create_new(self, genome_type, genome_config, num_genomes): new_genomes = {} for i in range(num_genomes): key = self.genome_indexer.get_next() g = genome_type.create(genome_config, key) new_genomes[key] = g self.ancestors[key] = tuple() return new_genomes def reproduce(self, config, species, pop_size): # 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 and collect the set of non-stagnated species members. num_remaining = 0 species_fitness = [] avg_adjusted_fitness = 0.0 for sid, s, stagnant in self.stagnation.update(species.species): if stagnant: self.reporters.species_stagnant(sid, s) else: num_remaining += 1 # Compute adjusted fitness. species_sum = 0.0 for m in itervalues(s.members): af = m.fitness / len(s.members) species_sum += af sfitness = species_sum / len(s.members) species_fitness.append((sid, s, sfitness)) avg_adjusted_fitness += sfitness # No species left. if 0 == num_remaining: species.species = {} return [] avg_adjusted_fitness /= len(species_fitness) self.reporters.info("Average adjusted fitness: {:.3f}".format(avg_adjusted_fitness)) # Compute the number of new individuals to create for the new generation. spawn_amounts = [] for sid, s, sfitness in species_fitness: spawn = len(s.members) if sfitness > avg_adjusted_fitness: spawn *= 1.1 else: spawn *= 0.9 spawn_amounts.append(spawn) # 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 = [int(round(n * norm)) for n in spawn_amounts] self.reporters.info("Spawn amounts: {0}".format(spawn_amounts)) self.reporters.info('Species fitness : {0!r}'.format([sfitness for sid, s, sfitness in species_fitness])) new_population = {} species.species = {} for spawn, (sid, s, sfitness) in zip(spawn_amounts, species_fitness): # If elitism is enabled, each species always at least gets to retain its elites. spawn = max(spawn, self.elitism) if spawn <= 0: continue # The species has at least one member for the next generation, so retain it. old_members = list(iteritems(s.members)) s.members = {} species.species[sid] = 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.elitism > 0: for i, m in old_members[:self.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.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). gid = self.genome_indexer.get_next() child = parent1.crossover(parent2, gid, config.genome_config) child.mutate(config.genome_config) new_population[gid] = child self.ancestors[gid] = (parent1_id, parent2_id) # Remove empty species from the stagnation tracking. keys = list(iterkeys(self.stagnation.stagnant_counts)) for sid in keys: if sid not in species.species: self.stagnation.remove(sid) # Sort species by ID (purely for ease of reading the reported list). # TODO: This should probably be done by the species object. #species.species.sort(key=lambda sp: sp.ID) return new_population
def __init__(self, config, reporters): self.reporters = reporters self.indexer = Indexer(1) self.species = {} self.genome_to_species = {}
def __init__(self, config): self.indexer = Indexer(1) self.species = {} self.to_species = {}
def __init__(self, config): self.config = config self.indexer = Indexer(1) self.species = []
class DefaultReproduction(object): """ Handles creation of genomes, either from scratch or by sexual or asexual reproduction from parents. Implements the default NEAT-python reproduction scheme: explicit fitness sharing with fixed-time species stagnation. """ def __init__(self, config, reporters): self.config = config params = config.get_type_config(self) self.elitism = int(params.get('elitism')) self.survival_threshold = float(params.get('survival_threshold')) self.reporters = reporters self.genome_indexer = Indexer(1) self.stagnation = config.stagnation_type(config, reporters) def create_new(self, num_genomes): new_genomes = [] for i in range(num_genomes): g = self.config.genotype.create(self.genome_indexer.get_next(), self.config) new_genomes.append(g) return new_genomes def reproduce(self, species, pop_size): # TODO: I don't like this modification of the species object, # because it requires internal knowledge of the object. # Filter out stagnated species and collect the set of non-stagnated species members. remaining_species = {} species_fitness = [] avg_adjusted_fitness = 0.0 for s, stagnant in self.stagnation.update(species.species): if stagnant: self.reporters.species_stagnant(s) else: remaining_species[s.ID] = s # Compute adjusted fitness. species_sum = 0.0 for m in s.members: af = m.fitness / len(s.members) species_sum += af sfitness = species_sum / len(s.members) species_fitness.append((s, sfitness)) avg_adjusted_fitness += sfitness # No species left. if not remaining_species: species.species = [] return [] avg_adjusted_fitness /= len(species_fitness) self.reporters.info( "Average adjusted fitness: {:.3f}".format(avg_adjusted_fitness)) # Compute the number of new individuals to create for the new generation. spawn_amounts = [] for s, sfitness in species_fitness: spawn = len(s.members) if sfitness > avg_adjusted_fitness: spawn *= 1.1 else: spawn *= 0.9 spawn_amounts.append(spawn) # 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 = [int(round(n * norm)) for n in spawn_amounts] self.reporters.info("Spawn amounts: {0}".format(spawn_amounts)) self.reporters.info('Species fitness : {0!r}'.format( [sfitness for s, sfitness in species_fitness])) new_population = [] species.species = [] for spawn, (s, sfitness) in zip(spawn_amounts, species_fitness): # If elitism is enabled, each species always at least gets to retain its elites. spawn = max(spawn, self.elitism) if spawn <= 0: continue # The species has at least one member for the next generation, so retain it. old_members = s.members s.members = [] species.species.append(s) # Sort members in order of descending fitness. old_members.sort(reverse=True) # Transfer elites to new generation. if self.elitism > 0: new_population.extend(old_members[:self.elitism]) spawn -= self.elitism if spawn <= 0: continue # Only use the survival threshold fraction to use as parents for the next generation. repro_cutoff = int( math.ceil(self.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 = random.choice(old_members) 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). child = parent1.crossover(parent2, self.genome_indexer.get_next()) new_population.append(child.mutate()) # Sort species by ID (purely for ease of reading the reported list). # TODO: This should probably be done by the species object. species.species.sort(key=lambda sp: sp.ID) return new_population
class Population(object): """ Manages all the species """ def __init__(self, config, initial_population=None): """ :param config: Either a config.Config object or path to a configuration file. :param initial_population: """ # If config is not a Config object, assume it is a path to the config file. if not isinstance(config, Config): config = Config(config) self.config = config self.diversity = config.diversity_type(self.config) self.species_indexer = Indexer(1) self.genome_indexer = Indexer(1) self.species = [] self.generation_statistics = [] self.most_fit_genomes = [] self.generation = -1 self.total_evaluations = 0 if initial_population is None: initial_population = self._create_population() # Partition the population into species based on current configuration. self._speciate(initial_population) def load_checkpoint(self, filename): '''Resumes the simulation from a previous saved point.''' with gzip.open(filename) as f: print('Resuming from a previous point: ' + filename) (self.species, self.generation_statistics, self.most_fit_genomes, self.generation, random_state) = pickle.load(f) random.setstate(random_state) def save_checkpoint(self, filename=None): """ Save the current simulation state. """ if filename is None: filename = 'neat-checkpoint-{0}'.format(self.generation) with gzip.open(filename, 'w', compresslevel=5) as f: data = (self.species, self.generation_statistics, self.most_fit_genomes, self.generation, random.getstate()) pickle.dump(data, f, protocol=pickle.HIGHEST_PROTOCOL) def _create_population(self): # Create a collection of unconnected genomes with no hidden nodes. new_population = [] for i in range(self.config.pop_size): g_id = self.genome_indexer.next() g = self.config.genotype.create_unconnected(g_id, self.config) new_population.append(g) # Add hidden nodes if requested. if self.config.hidden_nodes > 0: for g in new_population: g.add_hidden_nodes(self.config.hidden_nodes) # Add connections based on initial connectivity type. if self.config.initial_connection == 'fs_neat': for g in new_population: g.connect_fs_neat() elif self.config.initial_connection == 'fully_connected': for g in new_population: g.connect_full() elif self.config.initial_connection == 'partial': for g in new_population: g.connect_partial(self.config.connection_fraction) return new_population def _speciate(self, population): """Place genomes into species by genetic similarity.""" for individual in population: # Find the species with the most similar representative. min_distance = None closest_species = None for s in self.species: distance = individual.distance(s.representative) if distance < self.config.compatibility_threshold: if min_distance is None or distance < min_distance: closest_species = s min_distance = distance if closest_species: closest_species.add(individual) else: # No species is similar enough, create a new species for this individual. self.species.append(Species(individual, self.species_indexer.next())) # Verify that no species are empty. for s in self.species: assert s.members def _log_stats(self, population): """ Gather data for visualization/reporting purposes. """ # Keep a deep copy of the best genome, so that future modifications to the genome # do not produce an unexpected change in statistics. self.most_fit_genomes.append(copy.deepcopy(max(population))) # Store the fitnesses of the members of each currently active species. species_stats = {} for s in self.species: species_stats[s.ID] = [c.fitness for c in s.members] self.generation_statistics.append(species_stats) def epoch(self, fitness_function, n): """ Runs NEAT's genetic algorithm for n epochs. """ t0 = time.time() # for saving checkpoints for g in range(n): self.generation += 1 if self.config.report: print('\n ****** Running generation {0} ****** \n'.format(self.generation)) gen_start = time.time() # Collect a list of all members from all species. population = [] for s in self.species: population.extend(s.members) # Evaluate individuals fitness_function(population) self.total_evaluations += len(population) # Gather statistics. self._log_stats(population) # Print some statistics best = self.most_fit_genomes[-1] if self.config.report: fit_mean = mean([c.fitness for c in population]) fit_std = stdev([c.fitness for c in population]) print('Population\'s average fitness: {0:3.5f} stdev: {1:3.5f}'.format(fit_mean, fit_std)) print('Best fitness: {0:3.5f} - size: {1!r} - species {2} - id {3}'.format(best.fitness, best.size(), best.species_id, best.ID)) print('Species length: {0:d} totaling {1:d} individuals'.format(len(self.species), sum( [len(s.members) for s in self.species]))) print('Species ID : {0!s}'.format([s.ID for s in self.species])) print('Species size : {0!s}'.format([len(s.members) for s in self.species])) print('Amount to spawn : {0!s}'.format([s.spawn_amount for s in self.species])) print('Species age : {0}'.format([s.age for s in self.species])) print('Species fitness : {0!r}'.format([s.get_average_fitness() for s in self.species])) print('Species no improv: {0!r}'.format([s.no_improvement_age for s in self.species])) # Saves the best genome from the current generation if requested. if self.config.save_best: with open('best_genome_' + str(self.generation), 'wb') as f: pickle.dump(best, f) # End when the fitness threshold is reached. if best.fitness >= self.config.max_fitness_threshold: if self.config.report: print('\nBest individual in epoch {0} meets fitness threshold - complexity: {1!r}'.format( self.generation, best.size())) break # Remove stagnated species. # TODO: Log species removal for visualization purposes. # 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. new_species = [] for s in self.species: s.update_stagnation() if s.no_improvement_age <= self.config.max_stagnation: new_species.append(s) else: if self.config.report: print("\n Species {0} with {1} members is stagnated: removing it".format(s.ID, len(s.members))) self.species = new_species # TODO: Break these out into user-configurable classes # 1. Species survival determination (currently hard-coded to be stagnation with a fixed number of generations). # 2. Species spawn allotment (currently provided in the diversity object). # Check for complete extinction. new_population = [] if not self.species: if self.config.report: print('All species extinct.') # If requested by the user, create a completely new population, # otherwise raise an exception. if self.config.reset_on_extinction: new_population = self._create_population() else: raise MassExtinctionException() else: # Compute spawn levels for all current species and then reproduce. self.diversity.compute_spawn_amount(self.species) for s in self.species: # Verify that all species received non-zero spawn counts, as the speciation mechanism # is intended to allow initially less fit species time to improve before making them # extinct via the stagnation mechanism. assert s.spawn_amount > 0 # The Species.reproduce keeps one random child as its new representative, and # returns the rest as a list, which must be sorted into species. new_population.extend(s.reproduce(self.config, self.genome_indexer)) self._speciate(new_population) if self.config.checkpoint_interval is not None and time.time() > t0 + 60 * self.config.checkpoint_interval: if self.config.report: print('Creating timed checkpoint file at generation: {0}'.format(self.generation)) self.save_checkpoint() # Update the checkpoint time. t0 = time.time() elif self.config.checkpoint_generation is not None and self.generation % self.config.checkpoint_generation == 0: if self.config.report: print('Creating generation checkpoint file at generation: {0}'.format(self.generation)) self.save_checkpoint() if self.config.report: print("Generation time: {0:.3f} sec".format(time.time() - gen_start))
class DefaultGenomeConfig(object): allowed_connectivity = ['unconnected', 'fs_neat', 'full', 'partial'] aggregation_function_defs = { 'sum': sum, 'max': max, 'min': min, 'product': product } def __init__(self, params): # Create full set of available activation functions. self.activation_defs = ActivationFunctionSet() self.activation_options = params.get('activation_options', 'sigmoid').strip().split() self.aggregation_options = params.get('aggregation_options', 'sum').strip().split() self._params = [ ConfigParameter('num_inputs', int), ConfigParameter('num_outputs', int), ConfigParameter('num_hidden', int), ConfigParameter('feed_forward', bool), ConfigParameter('compatibility_disjoint_coefficient', float), ConfigParameter('compatibility_weight_coefficient', float), ConfigParameter('conn_add_prob', float), ConfigParameter('conn_delete_prob', float), ConfigParameter('node_add_prob', float), ConfigParameter('node_delete_prob', float) ] # Gather configuration data from the gene classes. self.node_gene_type = params['node_gene_type'] self._params += self.node_gene_type.get_config_params() self.connection_gene_type = params['connection_gene_type'] self._params += self.connection_gene_type.get_config_params() # Use the configuration data to interpret the supplied parameters. for p in self._params: setattr(self, p.name, p.interpret(params)) # By convention, input pins have negative keys, and the output # pins have keys 0,1,... self.input_keys = [-i - 1 for i in range(self.num_inputs)] self.output_keys = [i for i in range(self.num_outputs)] self.connection_fraction = None # Verify that initial connection type is valid. self.initial_connection = params.get('initial_connection', 'unconnected') if 'partial' in self.initial_connection: c, p = self.initial_connection.split() self.initial_connection = c self.connection_fraction = float(p) if not (0 <= self.connection_fraction <= 1): raise Exception( "'partial' connection value must be between 0.0 and 1.0, inclusive." ) assert self.initial_connection in self.allowed_connectivity def add_activation(self, name, func): self.activation_defs.add(name, func) def save(self, f): if 'partial' in self.initial_connection: if not (0 <= self.connection_fraction <= 1): raise Exception( "'partial' connection value must be between 0.0 and 1.0, inclusive." ) f.write('initial_connection = {0} {1}\n'.format( self.initial_connection, self.connection_fraction)) else: f.write('initial_connection = {0}\n'.format( self.initial_connection)) assert self.initial_connection in self.allowed_connectivity write_pretty_params(f, self, self._params) def get_new_node_key(self, node_dict): if not hasattr(self, 'node_indexer'): self.node_indexer = Indexer(max(list(iterkeys(node_dict))) + 1) new_id = self.node_indexer.get_next() assert new_id not in node_dict return new_id
class DefaultReproduction(object): """ Handles creation of genomes, either from scratch or by sexual or asexual reproduction from parents. Implements the default NEAT-python reproduction scheme: explicit fitness sharing with fixed-time species stagnation. """ # TODO: Create a separate configuration class instead of using a dict (for consistency with other types). @classmethod def parse_config(cls, param_dict): config = {'elitism': 1, 'survival_threshold': 0.2} config.update(param_dict) return config @classmethod def write_config(cls, f, param_dict): elitism = param_dict.get('elitism', 1) f.write('elitism = {}\n'.format(elitism)) survival_threshold = param_dict.get('survival_threshold', 0.2) f.write('survival_threshold = {}\n'.format(survival_threshold)) def __init__(self, config, reporters, stagnation): self.elitism = int(config.get('elitism')) self.survival_threshold = float(config.get('survival_threshold')) self.reporters = reporters self.genome_indexer = Indexer(1) self.stagnation = stagnation self.ancestors = {} def create_new(self, genome_type, genome_config, num_genomes): new_genomes = {} for i in range(num_genomes): key = self.genome_indexer.get_next() g = genome_type(key) g.configure_new(genome_config) new_genomes[key] = g self.ancestors[key] = tuple() return new_genomes def reproduce(self, config, species, pop_size, generation): # TODO: I don't like this modification of the species and stagnation objects, # because it requires internal knowledge of the objects. # Find minimum/maximum fitness across the entire population, for use in # species adjusted fitness computation. all_fitnesses = [] for sid, s in iteritems(species.species): all_fitnesses.extend(m.fitness for m in itervalues(s.members)) 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. fitness_range = max(1.0, max_fitness - min_fitness) # 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. num_remaining = 0 species_fitness = [] avg_adjusted_fitness = 0.0 for sid, s, stagnant in self.stagnation.update(species, generation): if stagnant: self.reporters.species_stagnant(sid, s) else: num_remaining += 1 # Compute adjusted fitness. msf = mean([m.fitness for m in itervalues(s.members)]) s.adjusted_fitness = (msf - min_fitness) / fitness_range species_fitness.append((sid, s, s.fitness)) avg_adjusted_fitness += s.adjusted_fitness # No species left. if 0 == num_remaining: species.species = {} return [] avg_adjusted_fitness /= len(species_fitness) self.reporters.info( "Average adjusted fitness: {:.3f}".format(avg_adjusted_fitness)) # Compute the number of new individuals to create for the new generation. spawn_amounts = [] for sid, s, sfitness in species_fitness: spawn = len(s.members) if sfitness > avg_adjusted_fitness: spawn = max(spawn + 2, spawn * 1.1) else: spawn = max(spawn * 0.9, 2) spawn_amounts.append(spawn) # 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 = [int(round(n * norm)) for n in spawn_amounts] new_population = {} species.species = {} for spawn, (sid, s, sfitness) in zip(spawn_amounts, species_fitness): # If elitism is enabled, each species always at least gets to retain its elites. spawn = max(spawn, self.elitism) if spawn <= 0: continue # The species has at least one member for the next generation, so retain it. old_members = list(iteritems(s.members)) s.members = {} species.species[sid] = 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.elitism > 0: for i, m in old_members[:self.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.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). gid = self.genome_indexer.get_next() child = config.genome_type(gid) child.configure_crossover(parent1, parent2, config.genome_config) child.mutate(config.genome_config) new_population[gid] = child self.ancestors[gid] = (parent1_id, parent2_id) return new_population
class DefaultSpeciesSet(object): """ Encapsulates the default speciation scheme. """ def __init__(self, config): self.indexer = Indexer(1) self.species = {} self.to_species = {} # TODO: Create a separate configuration class instead of using a dict (for consistency with other types). @classmethod def parse_config(cls, param_dict): config = { 'compatibility_threshold': float(param_dict['compatibility_threshold']) } return config @classmethod def write_config(cls, f, param_dict): compatibility_threshold = param_dict['compatibility_threshold'] f.write( 'compatibility_threshold = {}\n'.format(compatibility_threshold)) def speciate(self, config, population): """ Place genomes into species by genetic similarity. Note that this method assumes the current representatives of the species are from the old generation, and that after speciation has been performed, the old representatives should be dropped and replaced with representatives from the new generation. If you violate this assumption, you should make sure other necessary parts of the code are updated to reflect the new behavior. """ assert type(population) is dict compatibility_threshold = config.species_set_config[ 'compatibility_threshold'] # Reset all species member lists. for s in itervalues(self.species): s.members.clear() self.to_species.clear() # Partition population into species based on genetic similarity. for key, individual in iteritems(population): # Find the species with the most similar representative. min_distance = sys.float_info.max closest_species = None closest_species_id = None for sid, s in iteritems(self.species): rep = s.representative distance = individual.distance(rep, config.genome_config) compatible = distance < compatibility_threshold if compatible and distance < min_distance: closest_species = s closest_species_id = sid min_distance = distance if closest_species is not None: closest_species.add(key, individual) self.to_species[key] = closest_species_id else: # No species is similar enough, create a new species for this individual. sid = self.indexer.get_next() self.species[sid] = Species(sid, key, individual) self.to_species[key] = sid # Only keep non-empty species. empty_species_ids = [] for sid, s in iteritems(self.species): if not s.members: empty_species_ids.append(sid) for sid in empty_species_ids: del self.species[sid] # Select a random current member as the new representative. for s in itervalues(self.species): s.representative = random.choice(list(s.members.values())) def get_species_id(self, individual_id): return self.to_species[individual_id] def get_species(self, individual_id): sid = self.to_species[individual_id] return self.species[sid]
class DefaultSpeciesSet(object): """ Encapsulates the default speciation scheme. """ def __init__(self, config, reporters): self.reporters = reporters self.indexer = Indexer(1) self.species = {} self.genome_to_species = {} # TODO: Create a separate configuration class instead of using a dict (for consistency with other types). @classmethod def parse_config(cls, param_dict): return { 'compatibility_threshold': float(param_dict['compatibility_threshold']) } @classmethod def write_config(cls, f, param_dict): compatibility_threshold = param_dict['compatibility_threshold'] f.write( 'compatibility_threshold = {}\n'.format(compatibility_threshold)) def speciate(self, config, population, generation): """ Place genomes into species by genetic similarity. Note that this method assumes the current representatives of the species are from the old generation, and that after speciation has been performed, the old representatives should be dropped and replaced with representatives from the new generation. If you violate this assumption, you should make sure other necessary parts of the code are updated to reflect the new behavior. """ assert type(population) is dict compatibility_threshold = config.species_set_config[ 'compatibility_threshold'] # Find the best representatives for each existing species. unspeciated = set(iterkeys(population)) #print(unspeciated) distances = GenomeDistanceCache(config.genome_config) new_representatives = {} new_members = {} for sid, s in iteritems(self.species): candidates = [] for gid in unspeciated: g = population[gid] d = distances(s.representative, g) candidates.append((d, g)) # The new representative is the genome closest to the current representative. rdist, new_rep = min(candidates, key=lambda x: x[0]) new_rid = new_rep.key new_representatives[sid] = new_rid new_members[sid] = [new_rid] unspeciated.remove(new_rid) #print("Chose {0} as representative of species {1}".format(new_rid, sid)) # Partition population into species based on genetic similarity. while unspeciated: gid = unspeciated.pop() g = population[gid] # Find the species with the most similar representative. candidates = [] for sid, rid in iteritems(new_representatives): rep = population[rid] d = distances(rep, g) if d < compatibility_threshold: candidates.append((d, sid)) if candidates: sdist, sid = min(candidates, key=lambda x: x[0]) new_members[sid].append(gid) else: # No species is similar enough, create a new species, using # this genome as its representative. sid = self.indexer.get_next() new_representatives[sid] = gid new_members[sid] = [gid] # Update species collection based on new speciation. self.genome_to_species = {} for sid, rid in iteritems(new_representatives): s = self.species.get(sid) if s is None: s = Species(sid, generation) self.species[sid] = s members = new_members[sid] for gid in members: self.genome_to_species[gid] = sid member_dict = dict((gid, population[gid]) for gid in members) s.update(population[rid], member_dict) # gdmean = mean(itervalues(distances)) # gdstdev = stdev(itervalues(distances)) # self.reporters.info('Mean genetic distance {0:.3f}, standard deviation {1:.3f}'.format(gdmean, gdstdev)) def get_species_id(self, individual_id): return self.genome_to_species[individual_id] def get_species(self, individual_id): sid = self.genome_to_species[individual_id] return self.species[sid]
class Population(object): """ This class implements the core NEAT algorithm. It maintains a list of Species instances, each of which contains a collection of Genome instances. """ def __init__(self, config, initial_population=None): """ :param config: Either a config.Config object or path to a configuration file. :param initial_population: Either an initial set of Genome instances to be used as the initial population, or None, in which case a randomized set of Genomes will be created automatically based on the configuration parameters. """ # If config is not a Config object, assume it is a path to the config file. if not isinstance(config, Config): config = Config(config) # Configure statistics and reporting as requested by the user. self.reporters = ReporterSet() if config.collect_statistics: self.statistics = StatisticsReporter() self.add_reporter(self.statistics) else: self.statistics = None if config.report: self.add_reporter(StdOutReporter()) self.config = config self.species_indexer = Indexer(1) self.genome_indexer = Indexer(1) self.reproduction = config.reproduction_type(self.config, self.reporters, self.genome_indexer) self.species = [] self.generation = -1 self.total_evaluations = 0 # Create a population if one is not given, then partition into species. if initial_population is None: initial_population = self._create_population() self._speciate(initial_population) def add_reporter(self, reporter): self.reporters.add(reporter) def remove_reporter(self, reporter): self.reporters.remove(reporter) def load_checkpoint(self, filename): '''Resumes the simulation from a previous saved point.''' self.reporters.loading_checkpoint(filename) with gzip.open(filename) as f: (self.species, self.generation, random_state) = pickle.load(f) random.setstate(random_state) def save_checkpoint(self, filename=None, checkpoint_type="user"): """ Save the current simulation state. """ if filename is None: filename = 'neat-checkpoint-{0}'.format(self.generation) self.reporters.saving_checkpoint(checkpoint_type, filename) with gzip.open(filename, 'w', compresslevel=5) as f: data = (self.species, self.generation, random.getstate()) pickle.dump(data, f, protocol=pickle.HIGHEST_PROTOCOL) def _create_population(self): # Create a collection of unconnected genomes with no hidden nodes. new_population = [] # TODO: The genotype class should know how to do everything below, based # solely on what's in the config object. This allows users to completely # replace the initial population creation scheme if they choose. for i in range(self.config.pop_size): g_id = self.genome_indexer.get_next() g = self.config.genotype.create_unconnected(g_id, self.config) new_population.append(g) # Add hidden nodes if requested. if self.config.hidden_nodes > 0: for g in new_population: g.add_hidden_nodes(self.config.hidden_nodes) # Add connections based on initial connectivity type. if self.config.initial_connection == 'fs_neat': for g in new_population: g.connect_fs_neat() elif self.config.initial_connection == 'fully_connected': for g in new_population: g.connect_full() elif self.config.initial_connection == 'partial': for g in new_population: g.connect_partial(self.config.connection_fraction) return new_population def _speciate(self, population): """ Place genomes into species by genetic similarity. Note that this method assumes the current representatives of the species are from the old generation, and that after speciation has been performed, the old representatives should be dropped and replaced with representatives from the new generation. If you violate this assumption, you should make sure other necessary parts of the code are updated to reflect the new behavior. """ for individual in population: # Find the species with the most similar representative. min_distance = None closest_species = None for s in self.species: distance = individual.distance(s.representative) if distance < self.config.compatibility_threshold \ and (min_distance is None or distance < min_distance): closest_species = s min_distance = distance if closest_species: closest_species.add(individual) else: # No species is similar enough, create a new species for this individual. self.species.append( Species(individual, self.species_indexer.get_next())) # Only keep non-empty species. self.species = [s for s in self.species if s.members] # Select a random current member as the new representative. for s in self.species: s.representative = random.choice(s.members) def run(self, fitness_function, n): """ Runs NEAT's genetic algorithm for n generations. The user-provided fitness_function should take one argument, a list of all genomes in the population, and its return value is ignored. This function is free to maintain external state, perform evaluations in parallel, and probably any other thing you want. The only requirement is that each individual's fitness member must be set to a floating point value after this function returns. It is assumed that fitness_function does not modify the list of genomes, or the genomes themselves, apart from updating the fitness member. """ # Remember start time for saving timed checkpoints. last_checkpoint = time.time() for g in range(n): self.generation += 1 self.reporters.start_generation(self.generation) # Collect a list of all members from all species. population = [] for s in self.species: population.extend(s.members) # Evaluate all individuals in the population using the user-provided function. # TODO: Add an option to only evaluate each genome once, to reduce number of # fitness evaluations in cases where the fitness is known to be the same if the # genome doesn't change--in these cases, evaluating unmodified elites in each # generation is a waste of time. fitness_function(population) self.total_evaluations += len(population) # Gather and report statistics. best = max(population) self.reporters.post_evaluate(population, self.species, best) # Save the best genome from the current generation if requested. if self.config.save_best: with open('best_genome_' + str(self.generation), 'wb') as f: pickle.dump(best, f) # End if the fitness threshold is reached. if best.fitness >= self.config.max_fitness_threshold: self.reporters.found_solution(self.generation, best) break # Create the next generation from the current generation. self.species, new_population = self.reproduction.reproduce( self.species, self.config.pop_size) # Check for complete extinction if not self.species: self.reporters.complete_extinction() # If requested by the user, create a completely new population, # otherwise raise an exception. if self.config.reset_on_extinction: new_population = self._create_population() else: raise CompleteExtinctionException() # Update species age. for s in self.species: s.age += 1 # Divide the new population into species. self._speciate(new_population) # Save checkpoints if necessary. if self.config.checkpoint_time_interval is not None: timed_checkpoint_due = last_checkpoint + 60 * self.config.checkpoint_time_interval if time.time() >= timed_checkpoint_due: self.save_checkpoint(checkpoint_type="timed") last_checkpoint = time.time() if self.config.checkpoint_gen_interval is not None \ and self.generation % self.config.checkpoint_gen_interval == 0: self.save_checkpoint(checkpoint_type="generation") self.reporters.end_generation()
class DefaultReproduction(object): """ Handles creation of genomes, either from scratch or by sexual or asexual reproduction from parents. Implements the default NEAT-python reproduction scheme: explicit fitness sharing with fixed-time species stagnation. """ def __init__(self, config, reporters): self.config = config params = config.get_type_config(self) self.elitism = int(params.get('elitism')) self.survival_threshold = float(params.get('survival_threshold')) self.reporters = reporters self.genome_indexer = Indexer(1) self.stagnation = config.stagnation_type(config, reporters) def create_new(self, num_genomes): new_genomes = [] for i in range(num_genomes): g = self.config.genotype.create(self.genome_indexer.get_next(), self.config) new_genomes.append(g) return new_genomes def reproduce(self, species, pop_size): # TODO: I don't like this modification of the species object, # because it requires internal knowledge of the object. # Filter out stagnated species and collect the set of non-stagnated species members. remaining_species = {} species_fitness = [] avg_adjusted_fitness = 0.0 for s, stagnant in self.stagnation.update(species.species): if stagnant: self.reporters.species_stagnant(s) else: remaining_species[s.ID] = s # Compute adjusted fitness. species_sum = 0.0 for m in s.members: af = m.fitness / len(s.members) species_sum += af sfitness = species_sum / len(s.members) species_fitness.append((s, sfitness)) avg_adjusted_fitness += sfitness # No species left. if not remaining_species: species.species = [] return [] avg_adjusted_fitness /= len(species_fitness) self.reporters.info("Average adjusted fitness: {:.3f}".format(avg_adjusted_fitness)) # Compute the number of new individuals to create for the new generation. spawn_amounts = [] for s, sfitness in species_fitness: spawn = len(s.members) if sfitness > avg_adjusted_fitness: spawn *= 1.1 else: spawn *= 0.9 spawn_amounts.append(spawn) # 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 = [int(round(n * norm)) for n in spawn_amounts] self.reporters.info("Spawn amounts: {0}".format(spawn_amounts)) self.reporters.info('Species fitness : {0!r}'.format([sfitness for s, sfitness in species_fitness])) new_population = [] species.species = [] for spawn, (s, sfitness) in zip(spawn_amounts, species_fitness): # If elitism is enabled, each species always at least gets to retain its elites. spawn = max(spawn, self.elitism) if spawn <= 0: continue # The species has at least one member for the next generation, so retain it. old_members = s.members s.members = [] species.species.append(s) # Sort members in order of descending fitness. old_members.sort(reverse=True) # Transfer elites to new generation. if self.elitism > 0: new_population.extend(old_members[:self.elitism]) spawn -= self.elitism if spawn <= 0: continue # Only use the survival threshold fraction to use as parents for the next generation. repro_cutoff = int(math.ceil(self.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 = random.choice(old_members) 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). child = parent1.crossover(parent2, self.genome_indexer.get_next()) new_population.append(child.mutate()) # Sort species by ID (purely for ease of reading the reported list). # TODO: This should probably be done by the species object. species.species.sort(key=lambda sp: sp.ID) return new_population