Introduction
Topologies (NEAT) is a robust algorithm launched in 2002 by Kenneth O. Stanley and Risto Miikkulainen from the College of Texas at Austin in this paper. NEAT algorithm launched a brand new thought to the usual neuro-evolution methods that advanced fixed-topology networks, by dynamically growing the complexity of the networks over the generations.
On this article, I’ll stroll by the NEAT algorithm and its implementation from scratch in Python, specializing in the algorithm’s design choices and the intricacies that make NEAT each elegant and difficult to breed. This text is meant for a technical viewers, acquainted with neural networks and the fundamentals of evolutionary computation.
Evolutionary Algorithms: A Common Overview
Earlier than leaping into NEAT, let’s evaluate the evolutionary algorithm’s fundaments. Impressed by genetics and pure choice, evolutionary algorithms are a sort of optimization algorithms that resolve complicated issues by iteratively bettering a inhabitants of candidate options.
The core thought is to mimic the method of organic evolution:
- Initialization: The method begins by producing an preliminary inhabitants of candidate options. This preliminary options are generated randomly, and every resolution is usually represented as a “genome” or “chromosome”, which encodes its options and traits.
- Analysis: Every particular person within the inhabitants is evaluated in line with how effectively it solves the given downside. That is finished utilizing a health operate which assigns a numerical rating to every resolution. The upper the health, the higher the answer.
- Choice: Based mostly on their health, a subset of the inhabitants is chosen to change into the “dad and mom” of the subsequent technology. People with greater health scores have a better chance of being chosen.
- Replica: The chosen people then reproduce to create “offspring” for the subsequent technology utilizing genetic operators. There are two important processes on this part:
- Crossover: Two or extra father or mother genomes are mixed to provide new offspring genomes, merging advantageous traits.
- Mutation: Small adjustments are randomly launched into the offspring genomes. This introduces novelty and helps discover the search area, stopping the algorithm from getting caught in a neighborhood optima.
- Alternative: The newly generated offspring replaces the present inhabitants (both completely or partially), forming the brand new technology.
- Termination: Steps 2–5 are repeated for a hard and fast variety of generations, till a sure health threshold is reached, or till a passable resolution to the issue is discovered.
What Makes NEAT Particular?
NEAT stands out as a result of it goes additional than common evolutionary algorithms, which solely evolve the weights of the community. NEAT additionally evolves the topology of the networks, making them more and more complicated.
Earlier than NEAT, there have been two important challenges that didn’t permit for normal evolutionary algorithms for use to dinamically regulate the structure of the networks. NEAT solves these two challenges:
- Methods to carry out crossover between topologically various networks: In conventional genetic algorithms, combining two genomes with vastly totally different constructions usually results in non-functional or malformed offspring. Think about making an attempt to mix two totally different neural networks, the place one has three hidden layers and one other has just one, merely averaging weights or randomly merging connections will seemingly break the community’s performance.
- Methods to keep variety in a inhabitants with vastly totally different topologies: When networks can develop in complexity (including new nodes and connections), these structural mutations usually result in a short lived lower of their health. For instance, a brand new connection could intrude with the prevailing community’s performance earlier than its parameters are correctly tuned. Because of this, not too long ago mutated networks may be prematurely outcompeted by less complicated, extra optimized ones, even when the newer variant have the potential to evolve right into a superior resolution given extra time. This untimely convergence to native optima is a typical downside.
How NEAT Solves These Challenges
NEAT tackles these two basic issues by two ingenious mechanisms: historic markings (additionally referred to as innovation numbers) and speciation.
Historic Markings

To handle the problem of performing crossover between topologically various networks, NEAT introduces the idea of innovation numbers. When a brand new connection or node is added to a community throughout mutation, it’s assigned a globally distinctive innovation quantity. This quantity acts as a historic marking, indicating the historic origin of that specific genetic characteristic.
Let’s take a look at the instance within the picture above, the place we’ve got two networks, “Guardian 1” and “Guardian 2” present process crossover. We will see that each networks have a connection from node 2 to node 5, with innovation quantity 4. This tells us that this connection will need to have been inherited by each networks from a typical ancestor in some unspecified time in the future. Nonetheless, we will additionally see that “Guardian 1” has a connection (from node 1 to node 5) with the innovation quantity 8, however “Guardian 2” doesn’t have this connection. This exhibits that “Guardian 1” advanced this particular connection independently.
Throughout crossover, NEAT aligns the genes (nodes and connections) of the 2 dad and mom based mostly on their innovation numbers.
- Matching genes (these with the identical innovation quantity) are inherited randomly from both father or mother.
- Disjoint genes (current in a single father or mother however not the opposite, and with innovation numbers inside the vary of the opposite father or mother’s genes) are sometimes inherited from the fitter father or mother.
- Extra genes (current in a single father or mother however not the opposite, and with innovation numbers outdoors the vary of the opposite father or mother’s genes, that means they appeared later in evolutionary historical past) are additionally sometimes inherited from the fitter father or mother.
This entire course of ensures that functionally comparable elements of the networks are mixed accurately, even when their general constructions differ considerably.
Speciation
To keep up variety in a inhabitants with vastly totally different topologies, and stop untimely convergence, NEAT employs a way referred to as speciation. By means of speciation the inhabitants is split into totally different “species” based mostly on topological similarity. Networks inside the similar species usually tend to share frequent ancestral traits and thus, extra comparable constructions.
The similarity between two networks (genomes) is calculated utilizing a compatibility distance operate. This operate considers three parts:
- The variety of disjoint genes (genes current in a single genome however not the opposite, however inside the shared historic vary).
- The variety of extra genes (genes current in a single genome however not the opposite, and out of doors the shared historic vary).
- The common weight distinction of matching genes.
If the compatibility distance between two networks falls beneath a sure threshold, they’re thought of to belong to the identical species.
By speciating the inhabitants, NEAT ensures that:
- Competitors happens solely inside species: This protects novel or much less complicated constructions from being instantly outcompeted by extremely complicated however maybe presently sub-optimal designs.
- Every species has an opportunity to innovate and enhance: The very best people from every species are allowed to breed (elitism), selling the evolution of distinctive options inside totally different topological niches.
- Species can die out if they aren’t profitable: If a species constantly performs poorly, it’s going to shrink and finally disappear, making room for extra promising genetic traces.
Core Parts of NEAT
There are 4 core parts within the NEAT algorithm:
Node Genes
Every node gene represents a neuron of the neural community. Every node has:
- An ID (distinctive identifier)
- A layer: it may very well be enter, hidden, or output
- An activation fuction
- A bias worth
class NodeGene:
def __init__(self, id, layer, activation, bias):
self.id = id
self.layer = layer # The layer to which the node belongs
self.activation = activation # Activation operate
self.bias = bias
Connection Genes
The connection genes (synapses) symbolize the connections between the neurons of the community. Every connection gene has:
- Enter node ID
- Ouput node ID
- Weight
- Enabled flag (signifies whether or not the connection is enabled)
- Innovation quantity (distinctive identifier assigned when the connection is first created)
class ConnectionGene:
def __init__(self, in_node_id: int, out_node_id: int, weight: float, innov: int, enabled: bool = True):
self.in_node_id = in_node_id
self.out_node_id = out_node_id
self.weight = weight
self.enabled = enabled # Whether or not the connection is enabled or not
self.innov = innov # Innovation quantity described within the paper
Genomes
The genome is the “genetic blueprint” of a single neural community inside the NEAT algorithm. It’s basically a group of all of the node and connection genes that outline the community’s construction and parameters. With the Genome we will later assemble the precise runnable community (which we’ll name Phenotype). Every genome represents one particular person within the inhabitants.
class Genome:
def __init__(self, nodes, connections):
self.nodes = {node.id: node for node in nodes if node just isn't None}
self.connections = [c.copy() for c in connections]
self.health = 0
Innovation Tracker
A important part in any NEAT implementation is an Innovation Tracker. That is my customized implementation of this mechanism that’s liable for assigning and holding monitor of distinctive innovation numbers for each new connection and node created all through the method. This ensures that historic markers are constant throughout your complete inhabitants, which is key for accurately aligning genes throughout crossover.
class InnovationTracker:
def __init__(self):
self.current_innovation = 0
self.connection_innovations = {} # (in_node_id, out_node_id) -> innovation_number
self.node_innovations = {} # connection_innovation -> (node_innovation, conn1_innovation, conn2_innovation)
self.node_id_counter = 0
def get_connection_innovation(self, in_node_id, out_node_id):
"""Get innovation quantity for a connection, creating new if wanted"""
key = (in_node_id, out_node_id)
if key not in self.connection_innovations:
self.connection_innovations[key] = self.current_innovation
self.current_innovation += 1
return self.connection_innovations[key]
def get_node_innovation(self, connection_innovation):
"""Get innovation numbers for node insertion, creating new if wanted"""
if connection_innovation not in self.node_innovations:
# Create new node and two connections
node_id = self.node_id_counter
self.node_id_counter += 1
conn1_innovation = self.current_innovation
self.current_innovation += 1
conn2_innovation = self.current_innovation
self.current_innovation += 1
self.node_innovations[connection_innovation] = (node_id, conn1_innovation, conn2_innovation)
return self.node_innovations[connection_innovation]
NEAT Algorithm Circulation
With the core parts understood, now we will piece them collectively to grasp how NEAT works. Within the instance proven, the algorithm is making an attempt to resolve the XOR downside.
1. Initialization
The very first technology of genomes is all the time created with a quite simple and glued construction. This strategy aligns with NEAT’s philosophy of “beginning easy and rising complexity”, making certain it explores the only options first, progressively growing complexity.
On this code instance, we initialize the networks with the minimal construction: a fully-connected community with no hidden layers.
def create_initial_genome(num_inputs, num_outputs, innov: InnovationTracker):
input_nodes = []
output_nodes = []
connections = []
# Create enter nodes
for i in vary(num_inputs):
node = NodeGene(i, "enter", lambda x: x, 0)
input_nodes.append(node)
# Create output nodes
for i in vary(num_outputs):
node_id = num_inputs + i # We begin the ids the place we left within the earlier loop
node = NodeGene(node_id, "output", sigmoid, random.uniform(-1, 1))
output_nodes.append(node)
# Replace the innov tracker's node id
innov.node_id_counter = num_inputs + num_outputs
# Create connections
for i in vary(num_inputs):
for j in vary(num_outputs):
in_node_id = i
out_node_id = j + num_inputs
innov_num = innov.get_connection_innovation(in_node_id, out_node_id)
weight = random.uniform(-1, 1)
conn = ConnectionGene(in_node_id, out_node_id, weight, innov_num, True)
connections.append(conn)
all_nodes = input_nodes + output_nodes
return Genome(all_nodes, connections)
def create_initial_population(measurement, num_inputs, num_outputs, innov):
inhabitants = []
for _ in vary(measurement):
genome = create_initial_genome(num_inputs, num_outputs, innov)
inhabitants.append(genome)
return inhabitants
2. Consider Inhabitants Health
After the preliminary inhabitants is ready up, the NEAT algorithm enters the primary evolutionary loop. This loop repeats for a pre-defined variety of generations, or till one in every of its options reaches a health threshold. Every technology undergoes a collection of important steps: analysis, speciation, health adjustment, and copy. Step one is to guage the health of every particular person within the inhabitants. To do that first we’ve got to undergo the next steps:
- Phenotype Expression: For every Genome we first have to precise it into its phenotype (a runnable neural community). This entails establishing the precise neural community from the nodes and connections lists inside the Genome.
- Ahead Cross: As soon as the community is constructed, we carry out the ahead cross with the given inputs to provide the outputs.
- Health Calculation: Given the community’s enter and output we will now calculate it’s health utilizing the health operate. The health operate is problem-specific and is designed to return a numerical rating indicating how effectively the community achieved its purpose.
def topological_sort(edges):
""" Helper operate to kind the community's nodes """
visited = set()
order = []
def go to(n):
if n in visited:
return
visited.add(n)
for m in edges[n]:
go to(m)
order.append(n)
for node in edges:
go to(node)
return order[::-1]
class Genome:
... # The remainder of the strategies would go right here
def consider(self, input_values: listing[float]):
"""
Methodology of the Genome class.
Performs the phenotype expression and ahead cross
"""
node_values = {}
node_inputs = {n: [] for n in self.nodes}
input_nodes = [n for n in self.nodes.values() if n.layer == "input"]
output_nodes = [n for n in self.nodes.values() if n.layer == "output"]
# Confirm that the variety of enter values matches the variety of enter nodes
if len(input_nodes) != len(input_values):
increase ValueError(f"Variety of inputs does not match variety of enter nodes. Enter={len(input_nodes)}, num_in_val={len(input_values)}")
# Assign enter values
for node, val in zip(input_nodes, input_values):
node_values[node.id] = val
edges = {}
for n in self.nodes.values():
edges[n] = []
for conn in self.connections: # Solely assemble enabled connections
if conn.enabled:
in_node = self.get_node(conn.in_node_id)
out_node = self.get_node(conn.out_node_id)
edges[in_node].append(out_node)
node_inputs[conn.out_node_id].append(conn)
sorted_nodes = topological_sort(edges)
for node in sorted_nodes:
if node.id in node_values:
proceed
incoming = node_inputs[node.id]
total_input = sum(
node_values[c.in_node_id] * c.weight for c in incoming
) + node.bias
node_values[node.id] = node.activation(total_input)
return [node_values.get(n.id, 0) for n in output_nodes]
def fitness_xor(genome):
"""Calculate health for XOR downside"""
# XOR Drawback knowledge
X = [[0, 0], [0, 1], [1, 0], [1, 1]]
y = [0, 1, 1, 0]
total_error = 0
for i in vary(len(X)):
attempt:
output = genome.consider(X[i])
# print(f"Output: {output}")
if output:
error = abs(output[0] - y[i])
total_error += error
else:
error = y[i]
total_error += error
besides Exception as e:
print(f"Error: {e}")
return 0 # Return 0 health if analysis fails
health = 4 - total_error
return max(0, health)
3. Speciation
As an alternative of letting all people compete globally, NEAT divides the inhabitants into species, placing collectively these genomes which can be topologically comparable. This strategy prevents new topological improvements from being immediatly outcompeted by bigger, extra mature species, and permits them to mature.
The method of speciation in every technology entails:
- Measuring Compatibility: We use a compatibility distance operate to measure how totally different two Genomes are. The shorter the space between them, the extra comparable two genomes are. The next code implementation makes use of the system proposed within the authentic paper, with the proposed parameters.
def distance(genome1: Genome, genome2: Genome, c1=1.0, c2=1.0, c3=0.4):
genes1 = {g.innov: g for g in genome1.connections}
genes2 = {g.innov: g for g in genome2.connections}
innovations1 = set(genes1.keys())
innovations2 = set(genes2.keys())
matching = innovations1 & innovations2
disjoint = (innovations1 ^ innovations2)
extra = set()
max_innov1 = max(innovations1) if innovations1 else 0
max_innov2 = max(innovations2) if innovations2 else 0
max_innov = min(max_innov1, max_innov2)
# Separate extra from disjoint
for innov in disjoint.copy():
if innov > max_innov:
extra.add(innov)
disjoint.take away(innov)
# Weight distinction of matching genes
if matching:
weight_diff = sum(
abs(genes1[i].weight - genes2[i].weight) for i in matching
)
avg_weight_diff = weight_diff / len(matching)
else:
avg_weight_diff = 0
# Normalize by N
N = max(len(genome1.connections), len(genome2.connections))
if N < 20: # NEAT sometimes makes use of 1 if N < 20
N = 1
delta = (c1 * len(extra)) / N + (c2 * len(disjoint)) / N + c3 * avg_weight_diff
return delta
2. Grouping into Species: On the beggining of every technology, the Speciator is liable for categorizing all genomes into current or new species. Every species has one consultant genome, that serves because the benchmark towards which each and every particular person of the inhabitants is in comparison with decide if belongs to that species.
class Species:
def __init__(self, consultant: Genome):
self.consultant = consultant
self.members = [representative]
self.adjusted_fitness = 0
self.best_fitness = -float('inf')
self.stagnant_generations = 0
def add_member(self, genome: Genome):
self.members.append(genome)
def clear_members(self):
self.members = []
def update_fitness_stats(self):
if not self.members:
self.adjusted_fitness = 0
return
current_best_fitness = max(member.health for member in self.members)
# Verify for enchancment and replace stagnation
if current_best_fitness > self.best_fitness:
self.best_fitness = current_best_fitness
self.stagnant_generations = 0
else:
self.stagnant_generations += 1
self.adjusted_fitness = sum(member.health for member in self.members) / len(self.members)
class Speciator:
def __init__(self, compatibility_threshold=3.0):
self.species = []
self.compatibility_threshold = compatibility_threshold
def speciate(self, inhabitants: listing[Genome]):
""" Group genomes into species based mostly on distance """
# Clear all species for the brand new technology
for s in self.species:
s.clear_members()
for genome in inhabitants:
found_species = False
for species in self.species:
if distance(genome, species.consultant) < self.compatibility_threshold:
species.add_member(genome)
found_species = True
break
if not found_species:
new_species = Species(consultant=genome)
self.species.append(new_species)
# Take away empty species
self.species = [s for s in self.species if s.members]
# Recompute adjusted health
for species in self.species:
species.update_fitness_stats()
# Replace consultant to be one of the best member
species.consultant = max(species.members, key=lambda g: g.health)
def get_species(self):
return self.species
4. Adjusting Health
Even when genomes are grouped into species, a uncooked health worth isn’t sufficient to permit for honest copy. Bigger species would naturally produce extra offspring, probably overwhelming smaller species that may maintain promising, however nonetheless nascent, improvements. To counter this, NEAT employs the adjusted health, and adjusts the health based mostly on the species efficiency.
To regulate the health of a person, its health is split between the variety of people in its species. This mechanism is carried out within the update_fitness_stats methodology contained in the Species class.
5. Replica
After speciating and adjusting the fitnesses, the algorithm strikes to the copy part, the place the subsequent technology of genomes is created by a mix of choice, crossover, and mutation.
- Choice: On this implementation the choice is completed by elitism in the primary evolutionary loop.
2. Crossover: Some key points of this implementation are:
- Node inheritance: Enter and output nodes are explicitly ensured to be handed all the way down to the offspring. That is finished to make sure the performance of the community doesn’t break.
- Matching genes: When each dad and mom have a gene with the identical innovation quantity, one is chosen randomly. If the gene was disabled in both father or mother, there’s a 75% probability of the gene being disabled within the offspring.
- Extra genes: Extra genes from the much less match father or mother are usually not inherited.
def crossover(parent1: Genome, parent2: Genome) -> Genome:
""" Crossover assuming parent1 is the fittest father or mother """
offspring_connections = []
offspring_nodes = set()
all_nodes = {} # Gather all nodes from each dad and mom
for node in parent1.nodes.values():
all_nodes[node.id] = node.copy()
if node.layer in ("enter", "output"):
offspring_nodes.add(all_nodes[node.id]) # Make sure the enter and output nodes are included
for node in parent2.nodes.values():
if node.id not in all_nodes:
all_nodes[node.id] = node.copy()
# Construct maps of genes keyed by innovation quantity
genes1 = {g.innov: g for g in parent1.connections}
genes2 = {g.innov: g for g in parent2.connections}
# Mix all innovation numbers
all_innovs = set(genes1.keys()) | set(genes2.keys())
for innov in sorted(all_innovs):
gene1 = genes1.get(innov)
gene2 = genes2.get(innov)
if gene1 and gene2: # Matching genes
chosen = random.alternative([gene1, gene2])
gene_copy = chosen.copy()
if not gene1.enabled or not gene2.enabled: # 75% probability of the offsprign gene being disabled
if random.random() < 0.75:
gene_copy.enabled = False
elif gene1 and never gene2: # Disjoint gene (from the fittest father or mother)
gene_copy = gene1.copy()
else: # Not taking disjoint genes from much less match father or mother
proceed
# get nodes
in_node = all_nodes.get(gene_copy.in_node_id)
out_node = all_nodes.get(gene_copy.out_node_id)
if in_node and out_node:
offspring_connections.append(gene_copy)
offspring_nodes.add(in_node)
offspring_nodes.add(out_node)
offspring_nodes = listing(offspring_nodes) # Take away the duplicates
return Genome(offspring_nodes, offspring_connections)
3. Mutation: After crossover, mutation is utilized to the offspring. A key side of this implementation is that we keep away from forming cycles when including connections.
class Genome:
... # The remainder of the strategies would go right here
def _path_exists(self, start_node_id, end_node_id, checked_nodes=None):
""" Recursive operate to verify whether or not a apth between two nodes exists."""
if checked_nodes is None:
checked_nodes = set()
if start_node_id == end_node_id:
return True
checked_nodes.add(start_node_id)
for conn in self.connections:
if conn.enabled and conn.in_node_id == start_node_id:
if conn.out_node_id not in checked_nodes:
if self._path_exists(conn.out_node_id, end_node_id, checked_nodes):
return True
return False
def get_node(self, node_id):
return self.nodes.get(node_id, None)
def mutate_add_connection(self, innov: InnovationTracker):
node_list = listing(self.nodes.values())
# Attempt max 10 occasions
max_tries = 10
found_appropiate_nodes = False
for _ in vary(max_tries):
node1, node2 = random.pattern(node_list, 2)
if (node1.layer == "output" or (node1.layer == "hid" and node2.layer == "enter")):
node1, node2 = node2, node1 # Swap them
# Verify if it is making a loop to the identical node
if node1 == node2:
proceed
# Verify if it is making a connection between two nodes on the identical layer
if node1.layer == node2.layer:
proceed
if node1.layer == "output" or node2.layer == "enter":
proceed
# Verify whether or not the connection already exists
conn_exists=False
for c in self.connections:
if (c.in_node_id == node1.id and c.out_node_id == node2.id) or
(c.in_node_id == node2.id and c.out_node_id == node1.id):
conn_exists = True
break
if conn_exists:
proceed
# If there's a path from node2 to node1, then including a connection from node1 to node2 creates a cycle
if self._path_exists(node2.id, node1.id):
proceed
innov_num = innov.get_connection_innovation(node1.id, node2.id)
new_conn = ConnectionGene(node1.id, node2.id, random.uniform(-1, 1), innov_num, True)
self.connections.append(new_conn)
return
def mutate_add_node(self, innov: InnovationTracker):
enabled_conn = [c for c in self.connections if c.enabled]
if not enabled_conn:
return
connection = random.alternative(enabled_conn) # select a random enabled connectin
connection.enabled = False # Disable the connection
node_id, conn1_innov, conn2_innov = innov.get_node_innovation(connection.innov)
# Create node and connections
new_node = NodeGene(node_id, "hid", ReLU, random.uniform(-1,1))
conn1 = ConnectionGene(connection.in_node_id, node_id, 1, conn1_innov, True)
conn2 = ConnectionGene(node_id, connection.out_node_id, connection.weight, conn2_innov, True)
self.nodes[node_id] = new_node
self.connections.prolong([conn1, conn2])
def mutate_weights(self, charge=0.8, energy=0.5):
for conn in self.connections:
if random.random() < charge:
if random.random() < 0.1:
conn.weight = random.uniform(-1, 1)
else:
conn.weight += random.gauss(0, energy)
conn.weight = max(-5, min(5, conn.weight)) # Clamp weights
def mutate_bias(self, charge=0.7, energy=0.5):
for node in self.nodes.values():
if node.layer != "enter" and random.random() < charge:
if random.random() < 0.1:
node.bias = random.uniform(-1, 1)
else:
node.bias += random.gauss(0, energy)
node.bias = max(-5, min(5, node.bias))
def mutate(self, innov, conn_mutation_rate=0.05, node_mutation_rate=0.03, weight_mutation_rate=0.8, bias_mutation_rate=0.7):
self.mutate_weights(weight_mutation_rate)
self.mutate_bias(bias_mutation_rate)
if random.random() < conn_mutation_rate:
self.mutate_add_connection(innov)
if random.random() < node_mutation_rate:
self.mutate_add_node(innov)
6. Repeating the Course of Inside the Predominant Evolutionary Loop
As soon as all of the offspring has been generated and the brand new inhabitants is fashioned, the present technology ends, and the brand new inhabitants turns into the place to begin for the subsequent evolutionary cycle. That is dealt with by a important evolutionary loop, which orchestrates the entire algorithm.
def evolution(inhabitants, fitness_scores, speciator: Speciator, innov: InnovationTracker, stagnation_limit: int = 15):
new_population = []
# Assign health to genomes
for genome, health in zip(inhabitants, fitness_scores):
genome.health = health
# Speciate inhabitants
speciator.speciate(inhabitants)
species_list = speciator.get_species()
species_list.kind(key=lambda s: s.best_fitness, reverse=True) # Kind species by best_fitness
print(f"Species created: {len(species_list)}")
# Take away stagnant species
surviving_species = []
if species_list:
surviving_species.append(species_list[0]) # Maintain one of the best one no matter stagnation
for s in species_list[1:]:
if s.stagnant_generations < stagnation_limit:
surviving_species.append(s)
species_list = surviving_species
print(f"Species that survived: {len(species_list)}")
total_adjusted_fitness = sum(s.adjusted_fitness for s in species_list)
print(f"Complete adjusted health: {total_adjusted_fitness}")
# elitism
for species in species_list:
if species.members:
best_genome = max(species.members, key=lambda g: g.health)
new_population.append(best_genome)
remaining_offspring = len(inhabitants) - len(new_population)
# Allocate the remaining offspring
for species in species_list:
if total_adjusted_fitness > 0:
offspring_count = int((species.adjusted_fitness / total_adjusted_fitness) * remaining_offspring) # The fitter species can have extra offspring
else:
offspring_count = remaining_offspring // len(species_list) # If all of the species carried out poorly, assign offspring evenly between them
if offspring_count > 0:
offspring = reproduce_species(species, offspring_count, innov)
new_population.prolong(offspring)
# Guarantee there are sufficient people (we might have much less due to the rounding error)
whereas len(new_population) < len(inhabitants):
best_species = max(species_list, key=lambda s: s.adjusted_fitness)
offspring = reproduce_species(best_species, 1, innov)
new_population.prolong(offspring)
return new_population
7. Operating the Algorithm
def run_neat_xor(save_best=False, generations=50, pop_size=50, target_fitness=3.9, speciator_threshold=2.0):
NUM_INPUTS = 2
NUM_OUTPUTS = 1
# Initialize Innovation Quantity and Speciator
innov = InnovationTracker()
speciator = Speciator(speciator_threshold)
# Create preliminary inhabitants
inhabitants = create_initial_population(pop_size, NUM_INPUTS, NUM_OUTPUTS, innov)
# Stats
best_fitness_history = []
avg_fitness_history = []
species_count_history = []
# important loop
for gen in vary(generations):
fitness_scores = [fitness_xor(g) for g in population]
print(f"health: {fitness_scores}")
# get the stats
best_fitness = max(fitness_scores)
avg_fitness = sum(fitness_scores) / len(fitness_scores)
best_fitness_history.append(best_fitness)
avg_fitness_history.append(avg_fitness)
print(f"Era {gen}: Finest={best_fitness}, Avg={avg_fitness}")
# Verify if we achieved the goal health
if best_fitness >= target_fitness:
print(f"Drawback was solved in {gen} generations")
print(f"Finest health achieved: {max(best_fitness_history)}")
best_genome = inhabitants[fitness_scores.index(best_fitness)]
if save_best:
with open("best_genome.pkl", "wb") as f:
pickle.dump(best_genome, f)
return best_genome, best_fitness_history, avg_fitness_history, species_count_history
inhabitants = evolution(inhabitants, fitness_scores, speciator, innov)
print(f"Could not resolve the XOR downside in {generations} generations")
print(f"Finest health achieved: {max(best_fitness_history)}")
return None, best_fitness_history, avg_fitness_history, species_count_history