def compute_full_connections(self, config, direct): """ Compute connections for a fully-connected feed-forward genome--each input connected to all hidden nodes (and output nodes if ``direct`` is set or there are no hidden nodes), each hidden node connected to all output nodes. (Recurrent genomes will also include node self-connections.) """ hidden = [i for i in iterkeys(self.nodes) if i not in config.output_keys] output = [i for i in iterkeys(self.nodes) if i in config.output_keys] connections = [] if hidden: for input_id in config.input_keys: for h in hidden: connections.append((input_id, h)) for h in hidden: for output_id in output: connections.append((h, output_id)) if direct or (not hidden): for input_id in config.input_keys: for output_id in output: connections.append((input_id, output_id)) # For recurrent genomes, include node self-connections. if not config.feed_forward: for i in iterkeys(self.nodes): connections.append((i, i)) return connections
def mutate_add_connection(self, config): """ Attempt to add a new connection, the only restriction being that the output node cannot be one of the network input pins. """ possible_outputs = list(iterkeys(self.nodes)) out_node = choice(possible_outputs) possible_inputs = possible_outputs + config.input_keys in_node = choice(possible_inputs) # Don't duplicate connections. key = (in_node, out_node) if key in self.connections: # TODO: Should this be using mutation to/from rates? Hairy to configure... if config.check_structural_mutation_surer(): self.connections[key].enabled = True return # Don't allow connections between two output nodes if in_node in config.output_keys and out_node in config.output_keys: return # No need to check for connections between input nodes: # they cannot be the output end of a connection (see above). # For feed-forward networks, avoid creating cycles. if config.feed_forward and creates_cycle(list(iterkeys(self.connections)), key): return cg = self.create_connection(config, in_node, out_node) self.connections[cg.key] = cg
def distance(self, other, config): """ Returns the genetic distance between this genome and the other. This distance value is used to compute genome compatibility for speciation. """ # Compute node gene distance component. node_distance = 0.0 if self.nodes or other.nodes: disjoint_nodes = 0 for k2 in iterkeys(other.nodes): if k2 not in self.nodes: disjoint_nodes += 1 for k1, n1 in iteritems(self.nodes): n2 = other.nodes.get(k1) if n2 is None: disjoint_nodes += 1 else: # Homologous genes compute their own distance value. node_distance += n1.distance(n2, config) max_nodes = max(len(self.nodes), len(other.nodes)) node_distance = (node_distance + config.compatibility_disjoint_coefficient * disjoint_nodes) / max_nodes # Compute connection gene differences. connection_distance = 0.0 if self.connections or other.connections: disjoint_connections = 0 for k2 in iterkeys(other.connections): if k2 not in self.connections: disjoint_connections += 1 for k1, c1 in iteritems(self.connections): c2 = other.connections.get(k1) if c2 is None: disjoint_connections += 1 else: # Homologous genes compute their own distance value. connection_distance += c1.distance(c2, config) max_conn = max(len(self.connections), len(other.connections)) connection_distance = (connection_distance + config.compatibility_disjoint_coefficient * disjoint_connections) / max_conn distance = node_distance + connection_distance return distance
def end_generation(self, config, population, species_set): ng = len(population) ns = len(species_set.species) if self.show_species_detail: print('Population of {0:d} members in {1:d} species:'.format( ng, ns)) sids = list(iterkeys(species_set.species)) sids.sort() print(" ID age size fitness adj fit stag") print(" ==== === ==== ======= ======= ====") for sid in sids: s = species_set.species[sid] a = self.generation - s.created n = len(s.members) f = "--" if s.fitness is None else "{:.1f}".format(s.fitness) af = "--" if s.adjusted_fitness is None else "{:.3f}".format( s.adjusted_fitness) st = self.generation - s.last_improved print( " {: >4} {: >3} {: >4} {: >7} {: >7} {: >4}".format( sid, a, n, f, af, st)) else: print('Population of {0:d} members in {1:d} species'.format( ng, ns)) elapsed = time.time() - self.generation_start_time self.generation_times.append(elapsed) self.generation_times = self.generation_times[-10:] average = sum(self.generation_times) / len(self.generation_times) print('Total extinctions: {0:d}'.format(self.num_extinctions)) if len(self.generation_times) > 1: print("Generation time: {0:.3f} sec ({1:.3f} average)".format( elapsed, average)) else: print("Generation time: {0:.3f} sec".format(elapsed))
def test_fully_connected_hidden_direct(self): """full with direct input-output connections (and also via hidden hodes).""" gid = 42 config = self.config.genome_config config.initial_connection = 'full_direct' config.num_hidden = 2 g = neatfast.DefaultGenome(gid) self.assertEqual(gid, g.key) g.configure_new(config) print(g) self.assertEqual(set(iterkeys(g.nodes)), {0, 1, 2}) self.assertEqual(len(g.connections), 8) # Check that each input is connected to each hidden node. for i in config.input_keys: for h in (1, 2): assert ((i, h) in g.connections) # Check that each hidden node is connected to the output. for h in (1, 2): assert ((h, 0) in g.connections) # Check that inputs are directly connected to the output for i in config.input_keys: assert ((i, 0) in g.connections)
def test_fully_connected_hidden_nodirect_old(self): """ full (no specification re direct/nodirect) with hidden nodes; should output warning re docs/code conflict. """ gid = 42 config = self.config.genome_config config.initial_connection = 'full' config.num_hidden = 2 g = neatfast.DefaultGenome(gid) self.assertEqual(gid, g.key) print("\nThis should output a warning:", file=sys.stderr) g.configure_new(config) # TODO: Test for emitted warning print(g) self.assertEqual(set(iterkeys(g.nodes)), {0, 1, 2}) self.assertEqual(len(g.connections), 6) # Check that each input is connected to each hidden node. for i in config.input_keys: for h in (1, 2): assert ((i, h) in g.connections) # Check that each hidden node is connected to the output. for h in (1, 2): assert ((h, 0) in g.connections) # Check that inputs are not directly connected to the output for i in config.input_keys: assert ((i, 0) not in g.connections)
def get_new_node_key(self, node_dict): if self.node_indexer is None: self.node_indexer = count(max(list(iterkeys(node_dict))) + 1) new_id = next(self.node_indexer) assert new_id not in node_dict return new_id
def configure_new(self, config): # Create node genes for the output pins. for node_key in config.output_keys: self.nodes[node_key] = self.create_node(config, node_key) for input_id in config.input_keys: for node_id in iterkeys(self.nodes): connection = self.create_connection(config, input_id, node_id) self.connections[connection.key] = connection
def connect_fs_neat_hidden(self, config): """ Randomly connect one input to all hidden and output nodes (FS-NEAT with connections to hidden, if any). """ input_id = choice(config.input_keys) others = [i for i in iterkeys(self.nodes) if i not in config.input_keys] for output_id in others: connection = self.create_connection(config, input_id, output_id) self.connections[connection.key] = connection
def test_unconnected_hidden(self): """Unconnected network with hidden nodes.""" gid = 42 config = self.config.genome_config config.initial_connection = 'unconnected' config.num_hidden = 2 g = neatfast.DefaultGenome(gid) self.assertEqual(gid, g.key) g.configure_new(self.config.genome_config) print(g) self.assertEqual(set(iterkeys(g.nodes)), {0, 1, 2}) assert (not g.connections)
def test_fs_neat_nohidden(self): """fs_neat not connecting hidden nodes.""" gid = 42 config = self.config.genome_config config.initial_connection = 'fs_neat_nohidden' config.num_hidden = 2 g = neatfast.DefaultGenome(gid) self.assertEqual(gid, g.key) g.configure_new(config) print(g) self.assertEqual(set(iterkeys(g.nodes)), {0, 1, 2}) self.assertEqual(len(g.connections), 1)
def __init__(self, param_dict, param_list): self._params = param_list param_list_names = [] for p in param_list: setattr(self, p.name, p.interpret(param_dict)) param_list_names.append(p.name) unknown_list = [ x for x in iterkeys(param_dict) if x not in param_list_names ] if unknown_list: if len(unknown_list) > 1: raise UnknownConfigItemError("Unknown configuration items:\n" + "\n\t".join(unknown_list)) raise UnknownConfigItemError( "Unknown configuration item {!s}".format(unknown_list[0]))
def test_partially_connected_hidden_nodirect(self): """partial with no direct input-output connections, only via hidden nodes.""" gid = 42 config = self.config.genome_config config.initial_connection = 'partial_nodirect' config.connection_fraction = 0.5 config.num_hidden = 2 g = neatfast.DefaultGenome(gid) self.assertEqual(gid, g.key) g.configure_new(config) print(g) self.assertEqual(set(iterkeys(g.nodes)), {0, 1, 2}) self.assertLess(len(g.connections), 6)
def test_fs_neat_no_hidden(self): """ fs_neat with no hidden nodes (equivalent to fs_neat_hidden and fs_neat_nohidden with no hidden nodes). """ gid = 42 config = self.config.genome_config config.initial_connection = 'fs_neat' config.num_hidden = 0 g = neatfast.DefaultGenome(gid) self.assertEqual(gid, g.key) g.configure_new(config) print(g) self.assertEqual(set(iterkeys(g.nodes)), {0}) self.assertEqual(len(g.connections), 1)
def test_fs_neat_hidden_old(self): """ fs_neat (without hidden/nohidden specification) with hidden; should output warning about doc/code conflict. """ gid = 42 config = self.config.genome_config config.initial_connection = 'fs_neat' config.num_hidden = 2 g = neatfast.DefaultGenome(gid) self.assertEqual(gid, g.key) print("\nThis should output a warning:", file=sys.stderr) g.configure_new(config) # TODO: Test for emitted warning print(g) self.assertEqual(set(iterkeys(g.nodes)), {0, 1, 2}) self.assertEqual(len(g.connections), 1)
def test_partially_connected_no_hidden(self): """ partial with no hidden nodes (equivalent to partial_nodirect and partial_direct with no hidden nodes) """ gid = 42 config = self.config2.genome_config config.initial_connection = 'partial' config.connection_fraction = 0.5 config.num_hidden = 0 g = neatfast.DefaultGenome(gid) self.assertEqual(gid, g.key) g.configure_new(config) print(g) self.assertEqual(set(iterkeys(g.nodes)), {0}) self.assertLess(len(g.connections), 2)
def test_partially_connected_hidden_nodirect_old(self): """ partial (no specification re direct/nodirect) with hidden nodes; should output warning re docs/code conflict. """ gid = 42 config = self.config2.genome_config config.initial_connection = 'partial' config.connection_fraction = 0.5 config.num_hidden = 2 g = neatfast.DefaultGenome(gid) self.assertEqual(gid, g.key) print("\nThis should output a warning:", file=sys.stderr) g.configure_new(config) # TODO: Test for emitted warning print(g) self.assertEqual(set(iterkeys(g.nodes)), {0, 1, 2}) self.assertLess(len(g.connections), 6)
def mutate_delete_node(self, config): # Do nothing if there are no non-output nodes. available_nodes = [k for k in iterkeys(self.nodes) if k not in config.output_keys] if not available_nodes: return -1 del_key = choice(available_nodes) connections_to_delete = set() for k, v in iteritems(self.connections): if del_key in v.key: connections_to_delete.add(v.key) for key in connections_to_delete: del self.connections[key] del self.nodes[del_key] return del_key
def mutate_add_connection(self, config): ''' Attempt to add a new connection, the only restriction being that the output node cannot be one of the network input pins. ''' possible_outputs = list(iterkeys(self.nodes)) out_node = choice(possible_outputs) possible_inputs = possible_outputs + config.input_keys in_node = choice(possible_inputs) if in_node == out_node: return # # Don't duplicate connections. # key = (in_node, out_node) # if key in self.connections: # return cg = self.create_connection(config, in_node, out_node) self.connections[cg.key] = cg
def test_fully_connected_no_hidden(self): """ full with no hidden nodes (equivalent to full_nodirect and full_direct with no hidden nodes) """ gid = 42 config = self.config.genome_config config.initial_connection = 'full' config.num_hidden = 0 g = neatfast.DefaultGenome(gid) self.assertEqual(gid, g.key) g.configure_new(config) print(g) self.assertEqual(set(iterkeys(g.nodes)), {0}) self.assertEqual(len(g.connections), 2) # Check that each input is connected to the output node for i in config.input_keys: assert ((i, 0) in g.connections)
def __init__(self, name, **default_dict): self.name = name for n, default in iteritems(default_dict): self._config_items[n] = [self._config_items[n][0], default] for n in iterkeys(self._config_items): setattr(self, n + "_name", self.config_item_name(n))
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 isinstance(population, dict) compatibility_threshold = self.species_set_config.compatibility_threshold # Find the best representatives for each existing species. unspeciated = set(iterkeys(population)) 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. ignored_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) # 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: ignored_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 = next(self.indexer) 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.distances)) gdstdev = stdev(itervalues(distances.distances)) self.reporters.info( 'Mean genetic distance {0:.3f}, standard deviation {1:.3f}'.format( gdmean, gdstdev))
def get_config_params(self): return [ConfigParameter(self.config_item_name(n), self._config_items[n][0], self._config_items[n][1]) for n in iterkeys(self._config_items)]
def __init__(self, genome_type, reproduction_type, species_set_type, stagnation_type, filename): # Check that the provided types have the required methods. assert hasattr(genome_type, 'parse_config') assert hasattr(reproduction_type, 'parse_config') assert hasattr(species_set_type, 'parse_config') assert hasattr(stagnation_type, 'parse_config') self.genome_type = genome_type self.reproduction_type = reproduction_type self.species_set_type = species_set_type self.stagnation_type = stagnation_type if not os.path.isfile(filename): raise Exception('No such config file: ' + os.path.abspath(filename)) parameters = ConfigParser() with open(filename) as f: if hasattr(parameters, 'read_file'): parameters.read_file(f) else: parameters.readfp(f) # NEAT configuration if not parameters.has_section('NEAT'): raise RuntimeError( "'NEAT' section not found in NEAT configuration file.") param_list_names = [] for p in self.__params: if p.default is None: setattr(self, p.name, p.parse('NEAT', parameters)) else: try: setattr(self, p.name, p.parse('NEAT', parameters)) except Exception: setattr(self, p.name, p.default) warnings.warn( "Using default {!r} for '{!s}'".format( p.default, p.name), DeprecationWarning) param_list_names.append(p.name) param_dict = dict(parameters.items('NEAT')) unknown_list = [ x for x in iterkeys(param_dict) if x not in param_list_names ] if unknown_list: if len(unknown_list) > 1: raise UnknownConfigItemError( "Unknown (section 'NEAT') configuration items:\n" + "\n\t".join(unknown_list)) raise UnknownConfigItemError( "Unknown (section 'NEAT') configuration item {!s}".format( unknown_list[0])) # Parse type sections. genome_dict = dict(parameters.items(genome_type.__name__)) self.genome_config = genome_type.parse_config(genome_dict) species_set_dict = dict(parameters.items(species_set_type.__name__)) self.species_set_config = species_set_type.parse_config( species_set_dict) stagnation_dict = dict(parameters.items(stagnation_type.__name__)) self.stagnation_config = stagnation_type.parse_config(stagnation_dict) reproduction_dict = dict(parameters.items(reproduction_type.__name__)) self.reproduction_config = reproduction_type.parse_config( reproduction_dict)