Skip to content

Fix FeedForwardNetwork and output bug #282

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
6 changes: 4 additions & 2 deletions neat/checkpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class Checkpointer(BaseReporter):
to save and restore populations (and other aspects of the simulation state).
"""

def __init__(self, generation_interval=100, time_interval_seconds=300,
def __init__(self, generation_interval, time_interval_seconds=None,
filename_prefix='neat-checkpoint-'):
"""
Saves the current state (at the end of a generation) every ``generation_interval`` generations or
Expand Down Expand Up @@ -66,9 +66,11 @@ def save_checkpoint(self, config, population, species_set, generation):
pickle.dump(data, f, protocol=pickle.HIGHEST_PROTOCOL)

@staticmethod
def restore_checkpoint(filename):
def restore_checkpoint(filename, new_config=None):
"""Resumes the simulation from a previous saved point."""
with gzip.open(filename) as f:
generation, config, population, species_set, rndstate = pickle.load(f)
random.setstate(rndstate)
if new_config is not None:
config = new_config
return Population(config, (population, species_set, generation))
2 changes: 1 addition & 1 deletion neat/genome.py
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,7 @@ def mutate_add_connection(self, config):
possible_outputs = list(self.nodes)
out_node = choice(possible_outputs)

possible_inputs = possible_outputs + config.input_keys
possible_inputs = list((set(self.nodes)- set(config.output_keys)) | set(config.input_keys) )
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fixes new node from taking output nodes as inputs

in_node = choice(possible_inputs)

# Don't duplicate connections.
Expand Down
57 changes: 37 additions & 20 deletions neat/graphs.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""Directed graph algorithm implementations."""

from collections import defaultdict, deque

def creates_cycle(connections, test):
"""
Expand Down Expand Up @@ -38,20 +38,35 @@ def required_for_output(inputs, outputs, connections):
"""
assert not set(inputs).intersection(outputs)

# Create a graph representation of the connections
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The previous function wasn't working as intended

graph = defaultdict(list)
reverse_graph = defaultdict(list)
for a, b in connections:
graph[a].append(b)
reverse_graph[b].append(a)

# Perform a breadth-first search (BFS) from each input to find all reachable nodes
reachable = set(inputs)
queue = deque(inputs)

while queue:
node = queue.popleft()
for neighbor in graph[node]:
if neighbor not in reachable:
reachable.add(neighbor)
queue.append(neighbor)

# Now, traverse from the outputs and find all nodes that are required to reach the outputs
required = set(outputs)
s = set(outputs)
while 1:
# Find nodes not in s whose output is consumed by a node in s.
t = set(a for (a, b) in connections if b in s and a not in s)
while True:
# Find nodes not in s whose output is consumed by a node in s and is reachable from inputs
t = set(a for (a, b) in connections if b in s and a not in s and a in reachable)

if not t:
break

layer_nodes = set(x for x in t if x not in inputs)
if not layer_nodes:
break

required = required.union(layer_nodes)
required = required.union(t)
s = s.union(t)

return required
Expand All @@ -63,7 +78,6 @@ def feed_forward_layers(inputs, outputs, connections):
:param inputs: list of the network input nodes
:param outputs: list of the output node identifiers
:param connections: list of (input, output) connections in the network.

Returns a list of layers, with each layer consisting of a set of node identifiers.
Note that the returned layers do not contain nodes whose output is ultimately
never used to compute the final network output.
Expand All @@ -72,21 +86,24 @@ def feed_forward_layers(inputs, outputs, connections):
required = required_for_output(inputs, outputs, connections)

layers = []
s = set(inputs)
while 1:
potential_input = set(inputs)
while True:
# Find candidate nodes c for the next layer. These nodes should connect
# a node in s to a node not in s.
c = set(b for (a, b) in connections if a in s and b not in s)
c = set(b for (a, b) in connections if a in potential_input and b not in potential_input)
# Keep only the used nodes whose entire input set is contained in s.
t = set()
next_layer = set()
for n in c:
if n in required and all(a in s for (a, b) in connections if b == n):
t.add(n)
# select connections (a, b) where b == n
connections_to_n = [(a, b) for (a, b) in connections if b == n and a in required]
Copy link
Author

@Finebouche Finebouche May 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the change here is that a as to be in required nodes to be considered

if n in required and all(a in potential_input for (a, b) in connections_to_n):
next_layer.add(n)

if not t:
if not next_layer:
break

layers.append(t)
s = s.union(t)
layers.append(next_layer)
potential_input = potential_input.union(next_layer)

return layers, required

return layers
12 changes: 8 additions & 4 deletions neat/nn/feed_forward.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from neat.graphs import feed_forward_layers

import random

class FeedForwardNetwork(object):
def __init__(self, inputs, outputs, node_evals):
Expand All @@ -25,21 +25,25 @@ def activate(self, inputs):
return [self.values[i] for i in self.output_nodes]

@staticmethod
def create(genome, config):
def create(genome, config, unique_value=False, random_values=False):
""" Receives a genome and returns its phenotype (a FeedForwardNetwork). """

# Gather expressed connections.
connections = [cg.key for cg in genome.connections.values() if cg.enabled]

layers = feed_forward_layers(config.genome_config.input_keys, config.genome_config.output_keys, connections)
layers, required = feed_forward_layers(config.genome_config.input_keys, config.genome_config.output_keys, connections)
node_evals = []
for layer in layers:
for node in layer:
inputs = []
for conn_key in connections:
inode, onode = conn_key
if onode == node:
if onode == node and inode in required:
cg = genome.connections[conn_key]
if random_values:
cg.weight = random.uniform(-1.0, 1.0)
if unique_value:
cg.weight = unique_value
inputs.append((inode, cg.weight))

ng = genome.nodes[node]
Expand Down
10 changes: 5 additions & 5 deletions neat/parallel.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,17 @@
in order to evaluate multiple genomes at once.
"""
from multiprocessing import Pool

from tqdm import tqdm

class ParallelEvaluator(object):
def __init__(self, num_workers, eval_function, timeout=None, maxtasksperchild=None):
def __init__(self, num_workers, eval_function, timeout=None, initializer=None, initargs=(), maxtasksperchild=None):
"""
eval_function should take one argument, a tuple of (genome object, config object),
and return a single float (the genome's fitness).
"""
self.eval_function = eval_function
self.timeout = timeout
self.pool = Pool(processes=num_workers, maxtasksperchild=maxtasksperchild)
self.pool = Pool(processes=num_workers, maxtasksperchild=maxtasksperchild, initializer=initializer, initargs=initargs)

def __del__(self):
self.pool.close()
Expand All @@ -26,5 +26,5 @@ def evaluate(self, genomes, config):
jobs.append(self.pool.apply_async(self.eval_function, (genome, config)))

# assign the fitness back to each genome
for job, (ignored_genome_id, genome) in zip(jobs, genomes):
genome.fitness = job.get(timeout=self.timeout)
for job, (ignored_genome_id, genome) in tqdm(zip(jobs, genomes), total=len(jobs)):
genome.fitness = job.get(timeout=self.timeout)