/evolution.py
Python | 228 lines | 221 code | 5 blank | 2 comment | 4 complexity | a8eb199616c4f20f1af3649527636ebb MD5 | raw file
- import copy # For copying genome
- import collections # For making Population into a list with MutableSequence
- import logging
- import random
- next_gene_id = 0
- next_neuron_id = 0
- class Connection:
- """Connections are the genes of the individuals"""
- def __init__(self, from_neuron, to_neuron, weight, innovation, enabled=False):
- self.innovation = innovation
- self.from_neuron = from_neuron
- self.to_neuron = to_neuron
- self.weight = weight
- self.enabled = enabled
- def set_innovation(self, innovation):
- """Set the innovation number of the gene"""
- self.innovation = innovation
- def __eq__(self, other):
- """Check if another gene has the same innovation number"""
- if self.innovation == other.innovation:
- return True
- else:
- return False
- def get_neuron_values(neurons):
- value_list = []
- for neuron in neurons:
- value_list.append(neuron.value)
- return value_list
- def get_connection_values(connections):
- """Get the connection weight values as a matrix weights[input][output],
- where neurons is a list of the input neurons"""
- value_list = []
- for connection in connections:
- value_list.append(connection.weight)
- return value_list
- class Individual:
- def __init__(self, n_inputs, n_outputs, genome=None, neurons=None):
- """Constructor: Creates a new minimal individual, or from genome if supplied"""
- global next_gene_id
- global next_neuron_id
- self.mutate_rate = {'node': 0.1, 'connection': 0.1, 'weight': 0.1, 'enabled': 0.1}
- # Create neurons, neurons['inputs'][0] is a bias neuron
- if neurons:
- if n_inputs + 1 + n_outputs < len(neurons):
- raise ValueError("Not enough neurons supplied. "
- "Got {}, expected more than or equal to {}".format(len(neurons),
- n_inputs + 1 + n_outputs))
- self.neurons = copy.copy(neurons)
- else:
- self.neurons = {'inputs': [], 'hidden': [], 'outputs': []}
- for i in xrange(n_inputs + 1):
- self.neurons['inputs'].append(Neuron(next_neuron_id, (0, i)))
- next_neuron_id += 1
- self.neurons['inputs'][0].value = 1.0
- for i in xrange(n_outputs):
- self.neurons['outputs'].append(Neuron(next_neuron_id, (2, i)))
- next_neuron_id += 1
- # Create genome (i.e. connections)
- if genome:
- # Copy supplied genome
- if ((n_inputs + 1) * n_outputs) < len(genome):
- raise ValueError("Genome length is not large enough."
- " Expected {}, got {}.".format((n_inputs + 1) * n_outputs, len(genome)))
- self.genome = copy.copy(genome)
- else:
- # Create connections between inputs and outputs only
- self.genome = []
- for from_neuron in self.neurons['inputs']:
- for to_neuron in self.neurons['outputs']:
- self.add_connection(from_neuron, to_neuron)
- def get_all_neurons(self):
- """Returns a single list of all the neurons"""
- return self.neurons['inputs'] + self.neurons['hidden'] + self.neurons['outputs']
- def get_neuron(self, id_number):
- for neuron in self.get_all_neurons():
- if neuron.id == id_number:
- return neuron
- logging.warning("Neuron with id {} does not exist".format(id_number))
- return None
- def enable_all_connections(self):
- """Enable all connections of the network"""
- for connection in self.genome:
- connection.enabled = True
- def disable_all_connections(self):
- """Disable all connections of the network"""
- for connection in self.genome:
- connection.enabled = False
- def mutate(self):
- # TODO: Make sure the same mutation does not occur two times in the same generation
- # Add new nodes
- if random.random() < self.mutate_rate['node']:
- random.sample(self.genome)
- # Add new connections
- # Change weights
- # Disable/enable connections
- pass
- def add_connection(self, from_neuron, to_neuron, weight=1, enabled=False):
- """Add a connection between two neurons"""
- for connection in from_neuron.connections['outgoing']:
- if connection.to_neuron == to_neuron:
- logging.info("Connection already exists; innovation nr: " + str(connection.innovation))
- return
- global next_gene_id
- new_connection = Connection(from_neuron, to_neuron, weight, next_gene_id, enabled=enabled)
- from_neuron.add_outgoing(new_connection)
- to_neuron.add_incoming(new_connection)
- self.genome.append(new_connection)
- next_gene_id += 1
- return new_connection
- def add_neuron(self, connection):
- """Split the connection and add a neuron in the middle, creating two new connections.
- The old connection is disabled"""
- global next_neuron_id
- x_pos = (connection.to_neuron.position[0] + connection.from_neuron.position[0]) / 2.0
- y_pos = (connection.to_neuron.position[1] + connection.from_neuron.position[1]) / 2.0
- neuron = Neuron(next_neuron_id, (x_pos, y_pos))
- self.neurons['hidden'].append(neuron)
- self.add_connection(connection.from_neuron, neuron, 1, True)
- self.add_connection(neuron, connection.to_neuron, connection.weight, True)
- connection.enabled = False
- next_neuron_id += 1
- return neuron
- def reset_neurons(self):
- """Reset the values of all neurons to 0 (except for the bias neuron)"""
- for neuron in self.get_all_neurons()[1:]:
- # (self.neurons['inputs'][1:]+self.neurons['hidden']+self.neurons['outputs']):
- neuron.reset_value()
- def set_input_values(self, in_values):
- """Set the input values according to the specified list"""
- # TODO: Handle bias neuron
- if len(in_values) != (len(self.neurons['inputs'])-1):
- raise ValueError("Length of in-vector is not the same as the number of input neurons."
- "Got {}, expected {}".format(len(in_values),
- len(self.neurons['inputs'])-1))
- for i, value in enumerate(in_values):
- self.neurons['inputs'][i+1].value = value
- def feed_forward(self, in_values):
- """Do a feed-forward step of the network"""
- # Traverse the network recursively
- self.reset_neurons()
- self.set_input_values(in_values)
- output = []
- for neuron in self.neurons['outputs']:
- output.append(neuron.get_value())
- return output
- class Population(collections.MutableSequence):
- def __init__(self, pop_size, n_inputs, n_outputs):
- """Constructor: Initializes population as a list of individuals"""
- self._list = []
- self.mutate_rate = {'node': 0.5, 'link': 0.5, 'weights': 0.5}
- for i in xrange(pop_size):
- self._list.append(Individual(n_inputs, n_outputs))
- # Overloaded abstract functions
- def __getitem__(self, index):
- return self._list[index]
- def __len__(self):
- return len(self._list)
- def __setitem__(self, index, value):
- self._list[index] = value
- def insert(self, index, value):
- self._list.insert(index, value)
- def __delitem__(self, index):
- del self._list[index]
- class Neuron:
- def __init__(self, id_number, position, value=None):
- self.id = id_number # Unique ID
- self.value = value # The activated value of the neuron
- self.connections = {'incoming': [], 'outgoing': []} # Connections to and from the neuron
- self.position = position # Position, for drawing using netgraph
- def __eq__(self, other):
- if self.id == other.id:
- return True
- else:
- return False
- def add_incoming(self, connection):
- self.connections['incoming'].append(connection)
- def add_outgoing(self, connection):
- self.connections['outgoing'].append(connection)
- def reset_value(self):
- self.value = None
- def get_value(self):
- if not self.value:
- self.value = 0
- for connection in self.connections['incoming']:
- if connection.enabled:
- self.value += connection.weight*connection.from_neuron.get_value()
- return self.value