def mutate_disable_node(self, config: GenomeConfig): """Disable a node as part of a mutation, this is done by disabling all the node's adjacent connections.""" # Get a list of all possible nodes to deactivate (i.e. all the hidden, non-output, nodes) available_nodes = [ k for k in iterkeys(self.nodes) if k not in config.keys_output ] used_connections = self.get_used_connections() if not available_nodes: return # Find all the adjacent connections and disable those disable_key = choice(available_nodes) connections_to_disable = set() for _, v in iteritems(used_connections): if disable_key in v.key: connections_to_disable.add(v.key) # Check if any connections left after disabling node for k in connections_to_disable: used_connections.pop(k) _, _, _, used_conn = required_for_output( inputs={a for (a, _) in used_connections if a < 0}, outputs={i for i in range(self.num_outputs)}, connections=used_connections, ) # There are still connections left after disabling the nodes, disable connections for real if len(used_conn) > 0: for key in connections_to_disable: self.disable_connection(key=key, safe_disable=False)
def size(self): """Returns genome 'complexity', taken to be (number of hidden nodes, number of enabled connections)""" inputs = {a for (a, _) in self.connections.keys() if a < 0} _, used_hid, _, used_conn = required_for_output( inputs=inputs, outputs={i for i in range(self.num_outputs)}, connections=self.connections, ) return len(used_hid), len(used_conn)
def get_used_connections(self): """Get all of the connections currently used by the genome.""" connections = self.connections.copy() _, _, _, used_conn = required_for_output( inputs={a for (a, _) in connections if a < 0}, outputs={i for i in range(self.num_outputs)}, connections=connections, ) return used_conn
def get_used_nodes(self): """Get all of the nodes currently used by the genome.""" used_inp, used_hid, used_out, _ = required_for_output( inputs={a for (a, _) in self.connections if a < 0}, outputs={i for i in range(self.num_outputs)}, connections=self.connections, ) # used_nodes only is a set of node-IDs, transform this to a node-dictionary return { nid: n for (nid, n) in self.nodes.items() if nid in (used_inp | used_hid | used_out) }
def test_pruned2(self): """> Test a genome with a hidden recurrent node pruned from another hidden recurrent node.""" # Folder must be root to load in make_net properly if os.getcwd().split('\\')[-1] == 'tests': os.chdir('..') # Fetch the used genomes cfg = get_config() genome = get_pruned2(cfg) # Test the required nodes used_inp, used_hid, used_out, used_conn = required_for_output( inputs={a for (a, _) in genome.connections if a < 0}, outputs={i for i in range(cfg.genome.num_outputs)}, connections=genome.connections, ) # Two outputs, one used input, one used hidden self.assertEqual(len(used_inp), 1) self.assertEqual(len(used_hid), 1) self.assertEqual(len(used_out), 2) # Two used connections self.assertEqual(len(used_conn), 2)
def test_circular2(self): """> Test a genome with circular hidden nodes, only connected to the inputs.""" # Folder must be root to load in make_net properly if os.getcwd().split('\\')[-1] == 'tests': os.chdir('..') # Fetch the used genomes cfg = get_config() genome = get_circular2(cfg) # Test the required nodes used_inp, used_hid, used_out, used_conn = required_for_output( inputs={a for (a, _) in genome.connections if a < 0}, outputs={i for i in range(cfg.genome.num_outputs)}, connections=genome.connections, ) # Invalid genome self.assertEqual(len(used_inp), 0) self.assertEqual(len(used_hid), 0) self.assertEqual(len(used_out), 2) # No connections that were used to compute the outputs self.assertEqual(len(used_conn), 0)
def test_invalid2(self): """> Test a unconnected network that only has one hidden node with a recurrent connection attached.""" # Folder must be root to load in make_net properly if os.getcwd().split('\\')[-1] == 'tests': os.chdir('..') # Fetch the used genomes cfg = get_config() genome = get_invalid2(cfg) # Test the required nodes used_inp, used_hid, used_out, used_conn = required_for_output( inputs={a for (a, _) in genome.connections if a < 0}, outputs={i for i in range(cfg.genome.num_outputs)}, connections=genome.connections, ) # Number of nodes are only the two outputs self.assertEqual(len(used_inp), 0) self.assertEqual(len(used_hid), 0) self.assertEqual(len(used_out), 2) # No connections that were used to compute the outputs self.assertEqual(len(used_conn), 0)
def test_valid2(self): """> Test a partially connected network with a recurrent connection.""" # Folder must be root to load in make_net properly if os.getcwd().split('\\')[-1] == 'tests': os.chdir('..') # Fetch the used genomes cfg = get_config() genome = get_valid2(cfg) # Test the required nodes used_inp, used_hid, used_out, used_conn = required_for_output( inputs={a for (a, _) in genome.connections if a < 0}, outputs={i for i in range(cfg.genome.num_outputs)}, connections=genome.connections, ) # Number of nodes are two inputs, two outputs and one hidden node self.assertEqual(len(used_inp), 2) self.assertEqual(len(used_hid), 1) self.assertEqual(len(used_out), 2) # Three simple connections, and one recurrent self.assertEqual(len(used_conn), 3 + 1)
def test_valid1(self): """> Test a simple partial connected network.""" # Folder must be root to load in make_net properly if os.getcwd().split('\\')[-1] == 'tests': os.chdir('..') # Fetch the used genomes cfg = get_config() genome = get_valid1(cfg) # Test the required nodes used_inp, used_hid, used_out, used_conn = required_for_output( inputs={a for (a, _) in genome.connections if a < 0}, outputs={i for i in range(cfg.genome.num_outputs)}, connections=genome.connections, ) # Number of nodes are one input (only one used), two outputs (always present, even though not used!) self.assertEqual(len(used_inp), 1) self.assertEqual(len(used_hid), 0) self.assertEqual(len(used_out), 2) # Only one connection present in the network self.assertEqual(len(used_conn), 1)
def make_net(genome: Genome, genome_config: GenomeConfig, batch_size=1, initial_read: list = None, logger=None): """ This class will unravel the genome and create a feed-forward network based on it. In other words, it will create the phenotype (network) suiting the given genome. :param genome: The genome for which a network must be created :param genome_config: GenomeConfig object :param batch_size: Batch-size needed to setup network dimension :param initial_read: Initial sensory-input used to warm-up the network (no warm-up if None) :param logger: A population's logger """ # Collect the nodes whose state is required to compute the final network output(s), this excludes the inputs used_inp, used_hid, used_out, used_conn = required_for_output( inputs=set(genome_config.keys_input), outputs=set(genome_config.keys_output), connections=genome.connections) used_nodes: set = used_inp | used_hid | used_out if initial_read is not None: assert len(genome_config.keys_input) == len(initial_read) # Get a list of all the (used) input, (used) hidden, and output keys input_keys: np.ndarray = np.asarray(sorted(genome_config.keys_input)) hidden_keys: np.ndarray = np.asarray([ k for k in genome.nodes.keys() if (k not in genome_config.keys_output and k in used_nodes) ]) rnn_keys: np.ndarray = np.asarray([ k for k in hidden_keys if issubclass(genome.nodes[k].__class__, RnnNodeGene) ]) output_keys: np.ndarray = np.asarray(genome_config.keys_output) # Define the biases, note that inputs do not have a bias (since they aren't actually nodes!) hidden_biases: np.ndarray = np.asarray( [genome.nodes[k].bias for k in hidden_keys]) output_biases: np.ndarray = np.asarray( [genome.nodes[k].bias for k in output_keys]) # Create a mapping of a node's key to their index in their corresponding list input_k2i: dict = {k: i for i, k in enumerate(input_keys)} hidden_k2i: dict = {k: i for i, k in enumerate(hidden_keys)} output_k2i: dict = {k: i for i, k in enumerate(output_keys)} # Position-encode (index) the keys input_idx: np.ndarray = np.asarray([ k2i(k, input_k2i, input_keys, output_k2i, output_keys, hidden_k2i) for k in input_keys ]) hidden_idx: np.ndarray = np.asarray([ k2i(k, input_k2i, input_keys, output_k2i, output_keys, hidden_k2i) for k in hidden_keys ]) rnn_idx: np.ndarray = np.asarray([ k2i(k, input_k2i, input_keys, output_k2i, output_keys, hidden_k2i) for k in rnn_keys ]) output_idx: np.ndarray = np.asarray([ k2i(k, input_k2i, input_keys, output_k2i, output_keys, hidden_k2i) for k in output_keys ]) # Only feed-forward connections considered, these lists contain the connections and their weights respectively # Note that the connections are index-based and not key-based! in2hid: tuple = ([], []) hid2hid: tuple = ([], []) in2out: tuple = ([], []) hid2out: tuple = ([], []) # Convert the key-based connections to index-based connections one by one, also save their weights # At this point, it is already known that all connections are used connections for conn in used_conn.values(): # Convert to index-based i_key, o_key = conn.key i_idx: int = k2i(i_key, input_k2i, input_keys, output_k2i, output_keys, hidden_k2i) o_idx: int = k2i(o_key, input_k2i, input_keys, output_k2i, output_keys, hidden_k2i) # Store if i_key in input_keys and o_key in hidden_keys: idxs, vals = in2hid elif i_key in hidden_keys and o_key in hidden_keys: idxs, vals = hid2hid elif i_key in input_keys and o_key in output_keys: idxs, vals = in2out elif i_key in hidden_keys and o_key in output_keys: idxs, vals = hid2out else: msg = f"{genome}" \ f"\ni_key: {i_key}, o_key: {o_key}" \ f"\ni_key in input_keys: {i_key in input_keys}" \ f"\ni_key in hidden_keys: {i_key in hidden_keys}" \ f"\ni_key in output_keys: {i_key in output_keys}" \ f"\no_key in input_keys: {o_key in input_keys}" \ f"\no_key in hidden_keys: {o_key in hidden_keys}" \ f"\no_key in output_keys: {o_key in output_keys}" logger(msg) if logger else print(msg) raise ValueError( f'Invalid connection from key {i_key} to key {o_key}') # Append to the lists of the right tuple idxs.append((o_idx, i_idx)) # Connection: to, from vals.append(conn.weight) # Connection: weight # Create the RNN-cells and put them in a list rnn_array = np.asarray([]) rnn_map_temp = [] # Keep, otherwise errors occur for rnn_key in rnn_keys: # Query the node that contains the RNN cell's weights node = genome.nodes[rnn_key] # Create a map of all inputs/hidden nodes to the ones used by the RNN cell (as inputs) mapping = np.asarray([], dtype=bool) for k in input_keys: mapping = np.append(mapping, True if k in node.input_keys else False) for k in hidden_keys: mapping = np.append(mapping, True if k in node.input_keys else False) weight_map = np.asarray( [k in np.append(input_keys, hidden_keys) for k in node.input_keys]) # Add the RNN cell and its corresponding mapping to the list of used RNN cells rnn_array = np.append(rnn_array, node.get_rnn(mapping=weight_map)) assert len(mapping[mapping]) == rnn_array[-1].input_size rnn_map_temp.append(mapping) rnn_map = np.asarray(rnn_map_temp, dtype=bool) return FeedForwardNet( input_idx=input_idx, hidden_idx=hidden_idx, rnn_idx=rnn_idx, output_idx=output_idx, in2hid=in2hid, in2out=in2out, hid2hid=hid2hid, hid2out=hid2out, hidden_biases=hidden_biases, output_biases=output_biases, rnn_array=rnn_array, rnn_map=rnn_map, batch_size=batch_size, initial_read=initial_read, activation=sigmoid, )