From d627049de7f69ceb3a8a561ca6e09d8f9c7e1a99 Mon Sep 17 00:00:00 2001 From: Finebouche Date: Sat, 18 May 2024 14:30:25 +0200 Subject: [PATCH 01/12] Bug input node selection when adding connexion --- neat/genome.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/neat/genome.py b/neat/genome.py index 2d652650..2cf87806 100644 --- a/neat/genome.py +++ b/neat/genome.py @@ -341,9 +341,17 @@ 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) ) in_node = choice(possible_inputs) + # Check if Input node is an output node + if in_node in config.output_keys: + # Print usefull information + print("Output node: ", out_node) + print("Input node: ", in_node) + print("Possible inputs: ", possible_inputs) + raise Exception("Output node cannot be an input to a connection.") + # Don't duplicate connections. key = (in_node, out_node) if key in self.connections: From 24cdec1bcc8450ff3b1fec231e0eb2d776e3731f Mon Sep 17 00:00:00 2001 From: Finebouche Date: Mon, 20 May 2024 16:09:08 +0200 Subject: [PATCH 02/12] Bug in feedfroward network --- neat/graphs.py | 67 +++++++++++++++++++++++------------------ neat/nn/feed_forward.py | 4 +-- 2 files changed, 39 insertions(+), 32 deletions(-) diff --git a/neat/graphs.py b/neat/graphs.py index 0f3c6fc2..4dd3950e 100644 --- a/neat/graphs.py +++ b/neat/graphs.py @@ -1,5 +1,5 @@ """Directed graph algorithm implementations.""" - +from collections import defaultdict, deque def creates_cycle(connections, test): """ @@ -38,55 +38,62 @@ def required_for_output(inputs, outputs, connections): """ assert not set(inputs).intersection(outputs) + # Create a graph representation of the connections + 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 def feed_forward_layers(inputs, outputs, connections): - """ - Collect the layers whose members can be evaluated in parallel in a feed-forward network. - :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. - """ - 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] + 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 diff --git a/neat/nn/feed_forward.py b/neat/nn/feed_forward.py index df8fc967..5386683f 100644 --- a/neat/nn/feed_forward.py +++ b/neat/nn/feed_forward.py @@ -31,14 +31,14 @@ def create(genome, config): # 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] inputs.append((inode, cg.weight)) From 22e0a65cc3c0cd14de369eacad2eab75a59d2c05 Mon Sep 17 00:00:00 2001 From: Finebouche Date: Mon, 20 May 2024 16:10:33 +0200 Subject: [PATCH 03/12] Cleanup --- neat/genome.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/neat/genome.py b/neat/genome.py index 2cf87806..d096da84 100644 --- a/neat/genome.py +++ b/neat/genome.py @@ -344,14 +344,6 @@ def mutate_add_connection(self, config): possible_inputs = list((set(self.nodes)- set(config.output_keys)) | set(config.input_keys) ) in_node = choice(possible_inputs) - # Check if Input node is an output node - if in_node in config.output_keys: - # Print usefull information - print("Output node: ", out_node) - print("Input node: ", in_node) - print("Possible inputs: ", possible_inputs) - raise Exception("Output node cannot be an input to a connection.") - # Don't duplicate connections. key = (in_node, out_node) if key in self.connections: From 357b69fe4b73e7de85881378e7af44d93f7cbdf6 Mon Sep 17 00:00:00 2001 From: Finebouche Date: Mon, 20 May 2024 16:12:15 +0200 Subject: [PATCH 04/12] Readded function documentation --- neat/graphs.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/neat/graphs.py b/neat/graphs.py index 4dd3950e..da0e05a5 100644 --- a/neat/graphs.py +++ b/neat/graphs.py @@ -73,6 +73,16 @@ def required_for_output(inputs, outputs, connections): def feed_forward_layers(inputs, outputs, connections): + """ + Collect the layers whose members can be evaluated in parallel in a feed-forward network. + :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. + """ + required = required_for_output(inputs, outputs, connections) layers = [] From 7182716c6dda4871973b546e145e51eb7c1b37e0 Mon Sep 17 00:00:00 2001 From: Finebouche Date: Thu, 23 May 2024 16:34:54 +0200 Subject: [PATCH 05/12] Can now change configuration when using a checkpoint --- neat/checkpoint.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/neat/checkpoint.py b/neat/checkpoint.py index d2899e9a..ce8a8700 100644 --- a/neat/checkpoint.py +++ b/neat/checkpoint.py @@ -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)) From 5f119324eac7d9a77c3d9b6e95e221e9fedb7d8b Mon Sep 17 00:00:00 2001 From: Finebouche Date: Mon, 26 Aug 2024 11:56:37 +0200 Subject: [PATCH 06/12] Add WandbReporter --- neat/__init__.py | 1 + neat/wandb.py | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+) create mode 100644 neat/wandb.py diff --git a/neat/__init__.py b/neat/__init__.py index 8f80630a..5f89ab97 100644 --- a/neat/__init__.py +++ b/neat/__init__.py @@ -16,3 +16,4 @@ from neat.distributed import DistributedEvaluator, host_is_local from neat.threaded import ThreadedEvaluator from neat.checkpoint import Checkpointer +from neat.wandb import WandbReporter diff --git a/neat/wandb.py b/neat/wandb.py new file mode 100644 index 00000000..2b8cd4e4 --- /dev/null +++ b/neat/wandb.py @@ -0,0 +1,36 @@ +# based on reporting.py and statistics.py, make a WandbReporter +import wandb +from neat.reporting import BaseReporter + +class WandbReporter(BaseReporter): + def __init__(self, api_key, project_name, tags=None): + super().__init__() + self.api_key = api_key + self.project_name = project_name + self.tags = tags + + + def start_generation(self, generation): + wandb.init(project=self.project_name, tags=self.tags) + wandb.log({"generation": generation}) + + def end_generation(self, config, population, species_set): + pass + + def post_evaluate(self, config, population, species, best_genome): + wandb.log({"best_genome": best_genome.fitness}) + + def post_reproduction(self, config, population, species): + pass + + def complete_extinction(self): + pass + + def found_solution(self, config, generation, best): + pass + + def species_stagnant(self, sid, species): + pass + + def info(self, msg): + pass \ No newline at end of file From a6bff537b1ec24f91aeef1a55a2905a4739e9e35 Mon Sep 17 00:00:00 2001 From: Finebouche Date: Wed, 28 Aug 2024 11:43:38 +0200 Subject: [PATCH 07/12] Removed WandbReporter --- neat/__init__.py | 1 - neat/wandb.py | 36 ------------------------------------ 2 files changed, 37 deletions(-) delete mode 100644 neat/wandb.py diff --git a/neat/__init__.py b/neat/__init__.py index 5f89ab97..8f80630a 100644 --- a/neat/__init__.py +++ b/neat/__init__.py @@ -16,4 +16,3 @@ from neat.distributed import DistributedEvaluator, host_is_local from neat.threaded import ThreadedEvaluator from neat.checkpoint import Checkpointer -from neat.wandb import WandbReporter diff --git a/neat/wandb.py b/neat/wandb.py deleted file mode 100644 index 2b8cd4e4..00000000 --- a/neat/wandb.py +++ /dev/null @@ -1,36 +0,0 @@ -# based on reporting.py and statistics.py, make a WandbReporter -import wandb -from neat.reporting import BaseReporter - -class WandbReporter(BaseReporter): - def __init__(self, api_key, project_name, tags=None): - super().__init__() - self.api_key = api_key - self.project_name = project_name - self.tags = tags - - - def start_generation(self, generation): - wandb.init(project=self.project_name, tags=self.tags) - wandb.log({"generation": generation}) - - def end_generation(self, config, population, species_set): - pass - - def post_evaluate(self, config, population, species, best_genome): - wandb.log({"best_genome": best_genome.fitness}) - - def post_reproduction(self, config, population, species): - pass - - def complete_extinction(self): - pass - - def found_solution(self, config, generation, best): - pass - - def species_stagnant(self, sid, species): - pass - - def info(self, msg): - pass \ No newline at end of file From e67aaf860dc85a2d4841358c4cd18513cb7d481b Mon Sep 17 00:00:00 2001 From: Finebouche Date: Mon, 23 Sep 2024 09:43:21 +0200 Subject: [PATCH 08/12] Added initializer --- neat/parallel.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/neat/parallel.py b/neat/parallel.py index ac66acf1..ae1d0a1c 100644 --- a/neat/parallel.py +++ b/neat/parallel.py @@ -6,14 +6,14 @@ 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() From 91748a954db1dc68dfd271cdc5d377159912c5e7 Mon Sep 17 00:00:00 2001 From: Finebouche Date: Fri, 11 Oct 2024 15:14:18 +0200 Subject: [PATCH 09/12] add tqmd --- neat/parallel.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/neat/parallel.py b/neat/parallel.py index ae1d0a1c..dc380d4c 100644 --- a/neat/parallel.py +++ b/neat/parallel.py @@ -3,7 +3,7 @@ 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, initializer=None, initargs=(), maxtasksperchild=None): @@ -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) \ No newline at end of file From 4f58020a189709e1f5f7a6490c77d730ae391162 Mon Sep 17 00:00:00 2001 From: Finebouche Date: Sun, 13 Oct 2024 20:38:16 +0200 Subject: [PATCH 10/12] graph visualisation --- neat/checkpoint.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/neat/checkpoint.py b/neat/checkpoint.py index ce8a8700..f587ca06 100644 --- a/neat/checkpoint.py +++ b/neat/checkpoint.py @@ -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 From 7115af583a87b912eee67dda65736804bd5634ee Mon Sep 17 00:00:00 2001 From: Finebouche Date: Sun, 27 Oct 2024 19:23:16 +0100 Subject: [PATCH 11/12] add wann option for feedforward network activation --- neat/nn/feed_forward.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/neat/nn/feed_forward.py b/neat/nn/feed_forward.py index 5386683f..ef2f0f48 100644 --- a/neat/nn/feed_forward.py +++ b/neat/nn/feed_forward.py @@ -1,5 +1,5 @@ from neat.graphs import feed_forward_layers - +import random class FeedForwardNetwork(object): def __init__(self, inputs, outputs, node_evals): @@ -8,7 +8,7 @@ def __init__(self, inputs, outputs, node_evals): self.node_evals = node_evals self.values = dict((key, 0.0) for key in inputs + outputs) - def activate(self, inputs): + def activate(self, inputs, unique_value=False, random_values=False): if len(self.input_nodes) != len(inputs): raise RuntimeError("Expected {0:n} inputs, got {1:n}".format(len(self.input_nodes), len(inputs))) @@ -18,7 +18,12 @@ def activate(self, inputs): for node, act_func, agg_func, bias, response, links in self.node_evals: node_inputs = [] for i, w in links: - node_inputs.append(self.values[i] * w) + if random_values: + node_inputs.append(self.values[i] * random.uniform(-1, 1)) + elif unique_value: + node_inputs.append(self.values[i] * unique_value) + else: + node_inputs.append(self.values[i] * w) s = agg_func(node_inputs) self.values[node] = act_func(bias + response * s) From 5fd31422e16ba09a2ff33327ecf5ccd75997208f Mon Sep 17 00:00:00 2001 From: Finebouche Date: Sun, 27 Oct 2024 19:26:35 +0100 Subject: [PATCH 12/12] correct implementation for wann --- neat/nn/feed_forward.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/neat/nn/feed_forward.py b/neat/nn/feed_forward.py index ef2f0f48..adb61bff 100644 --- a/neat/nn/feed_forward.py +++ b/neat/nn/feed_forward.py @@ -8,7 +8,7 @@ def __init__(self, inputs, outputs, node_evals): self.node_evals = node_evals self.values = dict((key, 0.0) for key in inputs + outputs) - def activate(self, inputs, unique_value=False, random_values=False): + def activate(self, inputs): if len(self.input_nodes) != len(inputs): raise RuntimeError("Expected {0:n} inputs, got {1:n}".format(len(self.input_nodes), len(inputs))) @@ -18,19 +18,14 @@ def activate(self, inputs, unique_value=False, random_values=False): for node, act_func, agg_func, bias, response, links in self.node_evals: node_inputs = [] for i, w in links: - if random_values: - node_inputs.append(self.values[i] * random.uniform(-1, 1)) - elif unique_value: - node_inputs.append(self.values[i] * unique_value) - else: - node_inputs.append(self.values[i] * w) + node_inputs.append(self.values[i] * w) s = agg_func(node_inputs) self.values[node] = act_func(bias + response * s) 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. @@ -45,6 +40,10 @@ def create(genome, config): inode, onode = conn_key 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]