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
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 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
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.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 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 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 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 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 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 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
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