示例#1
0
    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)
示例#2
0
 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)
示例#3
0
 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
示例#4
0
 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)
     }
示例#5
0
    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)
示例#6
0
    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)
示例#7
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)
示例#8
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)
示例#9
0
    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)
示例#10
0
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,
    )