Close Menu
    Facebook LinkedIn YouTube WhatsApp X (Twitter) Pinterest
    Trending
    • Francis Bacon and the Scientific Method
    • Proxy-Pointer RAG: Structure Meets Scale at 100% Accuracy with Smarter Retrieval
    • Sulfur lava exoplanet L 98-59 d defies classification
    • Hisense U7SG TV Review (2026): Better Design, Great Value
    • Google is in talks with Marvell Technology to develop a memory processing unit that works alongside TPUs, and a new TPU for running AI models (Qianer Liu/The Information)
    • Premier League Soccer: Stream Man City vs. Arsenal From Anywhere Live
    • Dreaming in Cubes | Towards Data Science
    • Onda tiny house flips layout to fit three bedrooms and two bathrooms
    Facebook LinkedIn WhatsApp
    Times FeaturedTimes Featured
    Sunday, April 19
    • Home
    • Founders
    • Startups
    • Technology
    • Profiles
    • Entrepreneurs
    • Leaders
    • Students
    • VC Funds
    • More
      • AI
      • Robotics
      • Industries
      • Global
    Times FeaturedTimes Featured
    Home»Artificial Intelligence»From Genes to Neural Networks: Understanding and Building NEAT (Neuro-Evolution of Augmenting Topologies) from Scratch
    Artificial Intelligence

    From Genes to Neural Networks: Understanding and Building NEAT (Neuro-Evolution of Augmenting Topologies) from Scratch

    Editor Times FeaturedBy Editor Times FeaturedAugust 17, 2025No Comments21 Mins Read
    Facebook Twitter Pinterest Telegram LinkedIn Tumblr WhatsApp Email
    Share
    Facebook Twitter LinkedIn Pinterest Telegram Email WhatsApp Copy Link


    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 developed 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 selections and the intricacies that make NEAT each elegant and difficult to breed. This text is meant for a technical viewers, aware of neural networks and the fundamentals of evolutionary computation.

    Evolutionary Algorithms: A Normal Overview

    Earlier than leaping into NEAT, let’s overview the evolutionary algorithm’s fundaments. Impressed by genetics and pure choice, evolutionary algorithms are a kind of optimization algorithms that clear up complicated issues by iteratively enhancing a inhabitants of candidate options.

    The core thought is to mimic the method of organic evolution:

    1. 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.
    2. Analysis: Every particular person within the inhabitants is evaluated in keeping with how properly it solves the given drawback. That is achieved utilizing a health operate which assigns a numerical rating to every resolution. The upper the health, the higher the answer.
    3. Choice: Primarily based on their health, a subset of the inhabitants is chosen to develop into the “dad and mom” of the subsequent era. People with increased health scores have the next chance of being chosen.
    4. Replica: The chosen people then reproduce to create “offspring” for the subsequent era utilizing genetic operators. There are two foremost processes on this section:
      • Crossover: Two or extra dad or mum genomes are mixed to supply new offspring genomes, merging advantageous traits.
      • Mutation: Small modifications are randomly launched into the offspring genomes. This introduces novelty and helps discover the search house, stopping the algorithm from getting caught in a neighborhood optima.
    5. Alternative: The newly generated offspring replaces the present inhabitants (both totally or partially), forming the brand new era.
    6. 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 foremost challenges that didn’t enable for normal evolutionary algorithms for use to dinamically modify the structure of the networks. NEAT solves these two challenges:

    • How one can carry out crossover between topologically numerous networks: In conventional genetic algorithms, combining two genomes with vastly totally different buildings typically 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 doubtless break the community’s performance.
    • How one can keep range in a inhabitants with vastly totally different topologies: When networks can develop in complexity (including new nodes and connections), these structural mutations typically result in a brief lower of their health. For instance, a brand new connection might intrude with the prevailing community’s performance earlier than its parameters are correctly tuned. In consequence, not too long ago mutated networks may be prematurely outcompeted by easier, 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 standard drawback.

    How NEAT Solves These Challenges

    NEAT tackles these two basic issues by two ingenious mechanisms: historic markings (additionally known as innovation numbers) and speciation.

    Historic Markings

    Supply: Evolving Neural Networks through Augmenting Topologies

    To handle the problem of performing crossover between topologically numerous 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 exact genetic function.

    Let’s have a look at the instance within the picture above, the place we’ve two networks, “Mum or dad 1” and “Mum or dad 2” present process crossover. We are able to 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 standard ancestor sooner or later. Nonetheless, we are able to additionally see that “Mum or dad 1” has a connection (from node 1 to node 5) with the innovation quantity 8, however “Mum or dad 2” doesn’t have this connection. This exhibits that “Mum or dad 1” developed 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 dad or mum.
    • Disjoint genes (current in a single dad or mum however not the opposite, and with innovation numbers inside the vary of the opposite dad or mum’s genes) are usually inherited from the fitter dad or mum.
    • Extra genes (current in a single dad or mum however not the opposite, and with innovation numbers outdoors the vary of the opposite dad or mum’s genes, that means they appeared later in evolutionary historical past) are additionally usually inherited from the fitter dad or mum.

    This entire course of ensures that functionally comparable components of the networks are mixed accurately, even when their general buildings differ considerably.

    Speciation

    To keep up range in a inhabitants with vastly totally different topologies, and stop untimely convergence, NEAT employs a way known as speciation. Via speciation the inhabitants is split into totally different “species” based mostly on topological similarity. Networks inside the identical species usually tend to share frequent ancestral traits and thus, extra comparable buildings.

    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 outdoors 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 buildings from being instantly outcompeted by extremely complicated however maybe presently sub-optimal designs.
    • Every species has an opportunity to innovate and enhance: The most effective 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 don’t seem to be profitable: If a species constantly performs poorly, it’ll shrink and ultimately disappear, making room for extra promising genetic strains.

    Core Elements 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 could possibly 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 primarily a group of all of the node and connection genes that outline the community’s construction and parameters. With the Genome we are able to 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 shouldn't be None}
            self.connections = [c.copy() for c in connections]
            self.health = 0

    Innovation Tracker

    A important element in any NEAT implementation is an Innovation Tracker. That is my customized implementation of this mechanism that’s chargeable for assigning and maintaining 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 Move

    With the core parts understood, now we are able to piece them collectively to know how NEAT works. Within the instance proven, the algorithm is making an attempt to resolve the XOR drawback.

    1. Initialization

    The very first era of genomes is all the time created with a quite simple and stuck construction. This strategy aligns with NEAT’s philosophy of “beginning easy and rising complexity”, making certain it explores the only options first, step by step 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(dimension, num_inputs, num_outputs, innov):
        inhabitants = []
        for _ in vary(dimension):
            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 principle evolutionary loop. This loop repeats for a pre-defined variety of generations, or till one in all its options reaches a health threshold. Every era undergoes a sequence of important steps: analysis, speciation, health adjustment, and copy. Step one is to judge the health of every particular person within the inhabitants. To do that first we’ve to undergo the next steps:

    1. Phenotype Expression: For every Genome we first have to specific it into its phenotype (a runnable neural community). This entails setting up the precise neural community from the nodes and connections lists inside the Genome.
    2. Ahead Cross: As soon as the community is constructed, we carry out the ahead move with the given inputs to supply the outputs.
    3. Health Calculation: Given the community’s enter and output we are able to 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 properly the community achieved its aim.
    def topological_sort(edges):
      """ Helper operate to type 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: checklist[float]):
          """ 
              Methodology of the Genome class.
              Performs the phenotype expression and ahead move
          """
            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 would 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 drawback"""
        # 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 a substitute of letting all people compete globally, NEAT divides the inhabitants into species, placing collectively these genomes which might 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 era entails:

    1. 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 components 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 usually 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 era, the Speciator is chargeable for categorizing all genomes into current or new species. Every species has one consultant genome, that serves because the benchmark towards which each 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)
            
            # Examine 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: checklist[Genome]):
            """ Group genomes into species based mostly on distance """
            # Clear all species for the brand new era
            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 the perfect 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 truthful copy. Bigger species would naturally produce extra offspring, probably overwhelming smaller species which 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 section, the place the subsequent era of genomes is created by a mixture of choice, crossover, and mutation.

    1. Choice: On this implementation the choice is finished by elitism in the principle evolutionary loop.

    2. Crossover: Some key facets of this implementation are:

    • Node inheritance: Enter and output nodes are explicitly ensured to be handed right down to the offspring. That is achieved 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 dad or mum, there’s a 75% likelihood of the gene being disabled within the offspring.
    • Extra genes: Extra genes from the much less match dad or mum should not inherited.
    def crossover(parent1: Genome, parent2: Genome) -> Genome:
        """ Crossover assuming parent1 is the fittest dad or mum """
        offspring_connections = []
        offspring_nodes = set()
        all_nodes = {}  # Acquire 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.selection([gene1, gene2])
                gene_copy = chosen.copy()
    
                if not gene1.enabled or not gene2.enabled:  # 75% likelihood 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 dad or mum)
                gene_copy = gene1.copy()
            
            else:   # Not taking disjoint genes from much less match dad or mum
                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 = checklist(offspring_nodes) # Take away the duplicates
        
        return Genome(offspring_nodes, offspring_connections)

    3. Mutation: After crossover, mutation is utilized to the offspring. A key facet 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 test 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 = checklist(self.nodes.values())
    
            # Strive max 10 instances
            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
                # Examine if it is making a loop to the identical node
                if node1 == node2:
                    proceed
                # Examine 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
            
                # Examine 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.selection(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, price=0.8, energy=0.5):
            for conn in self.connections:
                if random.random() < price:
                    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, price=0.7, energy=0.5):
            for node in self.nodes.values():
                if node.layer != "enter" and random.random() < price:
                    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 Throughout the Essential Evolutionary Loop

    As soon as all of the offspring has been generated and the brand new inhabitants is fashioned, the present era ends, and the brand new inhabitants turns into the place to begin for the subsequent evolutionary cycle. That is dealt with by a foremost 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.type(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 the perfect 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 could 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 = []
        
        # foremost 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}: Greatest={best_fitness}, Avg={avg_fitness}")
            
            # Examine if we achieved the goal health
            if best_fitness >= target_fitness:
                print(f"Drawback was solved in {gen} generations")
                print(f"Greatest 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 clear up the XOR drawback in {generations} generations")
        print(f"Greatest health achieved: {max(best_fitness_history)}")
        return None, best_fitness_history, avg_fitness_history, species_count_history

    Full Code

    Github code



    Source link

    Share. Facebook Twitter Pinterest LinkedIn Tumblr Email
    Editor Times Featured
    • Website

    Related Posts

    Proxy-Pointer RAG: Structure Meets Scale at 100% Accuracy with Smarter Retrieval

    April 19, 2026

    Dreaming in Cubes | Towards Data Science

    April 19, 2026

    AI Agents Need Their Own Desk, and Git Worktrees Give Them One

    April 18, 2026

    Your RAG System Retrieves the Right Data — But Still Produces Wrong Answers. Here’s Why (and How to Fix It).

    April 18, 2026

    Europe Warns of a Next-Gen Cyber Threat

    April 18, 2026

    How to Learn Python for Data Science Fast in 2026 (Without Wasting Time)

    April 18, 2026

    Comments are closed.

    Editors Picks

    Francis Bacon and the Scientific Method

    April 19, 2026

    Proxy-Pointer RAG: Structure Meets Scale at 100% Accuracy with Smarter Retrieval

    April 19, 2026

    Sulfur lava exoplanet L 98-59 d defies classification

    April 19, 2026

    Hisense U7SG TV Review (2026): Better Design, Great Value

    April 19, 2026
    Categories
    • Founders
    • Startups
    • Technology
    • Profiles
    • Entrepreneurs
    • Leaders
    • Students
    • VC Funds
    About Us
    About Us

    Welcome to Times Featured, an AI-driven entrepreneurship growth engine that is transforming the future of work, bridging the digital divide and encouraging younger community inclusion in the 4th Industrial Revolution, and nurturing new market leaders.

    Empowering the growth of profiles, leaders, entrepreneurs businesses, and startups on international landscape.

    Asia-Middle East-Europe-North America-Australia-Africa

    Facebook LinkedIn WhatsApp
    Featured Picks

    San Francisco Mayor Daniel Lurie: ‘We Are a City on the Rise’

    December 9, 2025

    Best Heart Rate Monitors (2025), WIRED Tested and Reviewed

    August 24, 2025

    Bose Soundlink Plus Review: Compromise Never Sounded So Good

    July 5, 2025
    Categories
    • Founders
    • Startups
    • Technology
    • Profiles
    • Entrepreneurs
    • Leaders
    • Students
    • VC Funds
    Copyright © 2024 Timesfeatured.com IP Limited. All Rights.
    • Privacy Policy
    • Disclaimer
    • Terms and Conditions
    • About us
    • Contact us

    Type above and press Enter to search. Press Esc to cancel.