Close Menu
    Trending
    • Dispatch: Partying at one of Africa’s largest AI gatherings
    • Topp 10 AI-filmer genom tiderna
    • OpenAIs nya webbläsare ChatGPT Atlas
    • Creating AI that matters | MIT News
    • Scaling Recommender Transformers to a Billion Parameters
    • Hidden Gems in NumPy: 7 Functions Every Data Scientist Should Know
    • Is RAG Dead? The Rise of Context Engineering and Semantic Layers for Agentic AI
    • ChatGPT Gets More Personal. Is Society Ready for It?
    ProfitlyAI
    • Home
    • Latest News
    • AI Technology
    • Latest AI Innovations
    • AI Tools & Technologies
    • Artificial Intelligence
    ProfitlyAI
    Home » 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

    ProfitlyAIBy ProfitlyAIAugust 11, 2025No Comments21 Mins Read
    Share Facebook Twitter Pinterest LinkedIn Tumblr Reddit Telegram Email
    Share
    Facebook Twitter LinkedIn Pinterest Email


    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 concept to the usual neuro-evolution strategies that developed fixed-topology networks, by dynamically growing the complexity of the networks over the generations.

    On this article, I’ll stroll via 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 Basic Overview

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

    The core concept 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 answer 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 accordance with how nicely it solves the given downside. That is accomplished utilizing a health perform which assigns a numerical rating to every answer. The upper the health, the higher the answer.
    3. Choice: Based mostly on their health, a subset of the inhabitants is chosen to develop into the “mother and father” of the subsequent technology. People with increased health scores have a better chance of being chosen.
    4. Copy: The chosen people then reproduce to create “offspring” for the subsequent technology utilizing genetic operators. There are two major processes on this section:
      • Crossover: Two or extra mother or father 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.
    5. Alternative: The newly generated offspring replaces the present inhabitants (both solely or partially), forming the brand new technology.
    6. Termination: Steps 2–5 are repeated for a set variety of generations, till a sure health threshold is reached, or till a passable answer 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 major challenges that didn’t permit for normal evolutionary algorithms for use to dinamically modify the structure of the networks. NEAT solves these two challenges:

    • The way to carry out crossover between topologically numerous networks: In conventional genetic algorithms, combining two genomes with vastly completely different buildings typically results in non-functional or malformed offspring. Think about making an attempt to mix two completely different neural networks, the place one has three hidden layers and one other has just one, merely averaging weights or randomly merging connections will probably break the community’s performance.
    • The way to keep variety in a inhabitants with vastly completely 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 could intrude with the prevailing community’s performance earlier than its parameters are correctly tuned. In consequence, lately mutated networks could be prematurely outcompeted by easier, extra optimized ones, even when the newer variant have the potential to evolve right into a superior answer given extra time. This untimely convergence to native optima is a typical downside.

    How NEAT Solves These Challenges

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

    Historic Markings

    Supply: Evolving Neural Networks through Augmenting Topologies

    To deal with 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 individual genetic characteristic.

    Let’s have a look at the instance within the picture above, the place now we have two networks, “Dad or mum 1” and “Dad or mum 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 typical ancestor sooner or later. Nevertheless, we will additionally see that “Dad or mum 1” has a connection (from node 1 to node 5) with the innovation quantity 8, however “Dad or mum 2” doesn’t have this connection. This exhibits that “Dad or mum 1” developed this particular connection independently.

    Throughout crossover, NEAT aligns the genes (nodes and connections) of the 2 mother and father based mostly on their innovation numbers.

    • Matching genes (these with the identical innovation quantity) are inherited randomly from both mother or father.
    • Disjoint genes (current in a single mother or father however not the opposite, and with innovation numbers inside the vary of the opposite mother or father’s genes) are sometimes inherited from the fitter mother or father.
    • Extra genes (current in a single mother or father however not the opposite, and with innovation numbers outdoors the vary of the opposite mother or father’s genes, that means they appeared later in evolutionary historical past) are additionally sometimes inherited from the fitter mother or father.

    This complete course of ensures that functionally comparable elements of the networks are mixed appropriately, even when their total buildings differ considerably.

    Speciation

    To take care of variety in a inhabitants with vastly completely different topologies, and stop untimely convergence, NEAT employs a way known as speciation. By means of speciation the inhabitants is split into completely different “species” based mostly on topological similarity. Networks inside the similar species usually tend to share widespread ancestral traits and thus, extra comparable buildings.

    The similarity between two networks (genomes) is calculated utilizing a compatibility distance perform. This perform considers three elements:

    • 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 under a sure threshold, they’re thought-about 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 very best people from every species are allowed to breed (elitism), selling the evolution of distinctive options inside completely different topological niches.
    • Species can die out if they don’t seem to be profitable: If a species constantly performs poorly, it’s going to shrink and finally disappear, making room for extra promising genetic strains.

    Core Elements of NEAT

    There are 4 core elements 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 perform
            self.bias = bias

    Connection Genes

    The connection genes (synapses) signify 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 set 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 shouldn't be None}
            self.connections = [c.copy() for c in connections]
            self.health = 0

    Innovation Tracker

    A crucial part in any NEAT implementation is an Innovation Tracker. That is my customized implementation of this mechanism that’s answerable 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 the whole inhabitants, which is prime for appropriately 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 Movement

    With the core elements understood, now we will piece them collectively to grasp how NEAT works. Within the instance proven, the algorithm is making an attempt to unravel the XOR downside.

    1. Initialization

    The very first technology of genomes is at all times created with a quite simple and stuck construction. This method aligns with NEAT’s philosophy of “beginning easy and rising complexity”, making certain it explores the best 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 about 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 technology undergoes a sequence of crucial 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 now we have to undergo the next steps:

    1. Phenotype Expression: For every Genome we first have to precise it into its phenotype (a runnable neural community). This includes 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 provide the outputs.
    3. Health Calculation: Given the community’s enter and output we will now calculate it’s health utilizing the health perform. The health perform is problem-specific and is designed to return a numerical rating indicating how nicely the community achieved its objective.
    def topological_sort(edges):
      """ Helper perform 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]):
          """ 
              Technique 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):
                elevate 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 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 a substitute of letting all people compete globally, NEAT divides the inhabitants into species, placing collectively these genomes which can be topologically comparable. This method 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 includes:

    1. Measuring Compatibility: We use a compatibility distance perform to measure how completely different two Genomes are. The shorter the space between them, the extra comparable two genomes are. The next code implementation makes use of the method proposed within the unique 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 answerable for categorizing all genomes into present 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)
            
            # Test 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 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 the most effective 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 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. Copy

    After speciating and adjusting the fitnesses, the algorithm strikes to the copy section, the place the subsequent technology of genomes is created via a mixture of choice, crossover, and mutation.

    1. Choice: On this implementation the choice is completed via 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 accomplished to make sure the performance of the community doesn’t break.
    • Matching genes: When each mother and father have a gene with the identical innovation quantity, one is chosen randomly. If the gene was disabled in both mother or father, there’s a 75% probability of the gene being disabled within the offspring.
    • Extra genes: Extra genes from the much less match mother or father are usually not inherited.
    def crossover(parent1: Genome, parent2: Genome) -> Genome:
        """ Crossover assuming parent1 is the fittest mother or father """
        offspring_connections = []
        offspring_nodes = set()
        all_nodes = {}  # Gather all nodes from each mother and father
        
        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% 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 mother or father)
                gene_copy = gene1.copy()
            
            else:   # Not taking disjoint genes from much less match mother or father
                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 perform 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 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
                # Test if it is making a loop to the identical node
                if node1 == node2:
                    proceed
                # Test 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
            
                # Test 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 Inside the Important 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 major 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) # Type 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]) # Hold the most effective 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"Whole 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 may 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. Working 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 = []
        
        # major 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}")
            
            # Test 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

    Full Code

    Github code



    Source link

    Share. Facebook Twitter Pinterest LinkedIn Tumblr Email
    Previous ArticleLangGraph + SciPy: Building an AI That Reads Documentation and Makes Decisions
    Next Article Sam Altman and the whale
    ProfitlyAI
    • Website

    Related Posts

    Artificial Intelligence

    Creating AI that matters | MIT News

    October 21, 2025
    Artificial Intelligence

    Scaling Recommender Transformers to a Billion Parameters

    October 21, 2025
    Artificial Intelligence

    Hidden Gems in NumPy: 7 Functions Every Data Scientist Should Know

    October 21, 2025
    Add A Comment
    Leave A Reply Cancel Reply

    Top Posts

    How AI could speed the development of RNA vaccines and other RNA therapies | MIT News

    August 15, 2025

    LLM Optimization: LoRA and QLoRA | Towards Data Science

    May 30, 2025

    The two people shaping the future of OpenAI’s research

    July 31, 2025

    TDS Newsletter: September Must-Reads on ML Career Roadmaps, Python Essentials, AI Agents, and More

    October 9, 2025

    If we use AI to do our work – what is our job, then?

    September 12, 2025
    Categories
    • AI Technology
    • AI Tools & Technologies
    • Artificial Intelligence
    • Latest AI Innovations
    • Latest News
    Most Popular

    Why Your Prompts Don’t Belong in Git

    August 25, 2025

    “My biggest lesson was realizing that domain expertise matters more than algorithmic complexity.“

    August 14, 2025

    A Bird’s-Eye View of Linear Algebra: Measure of a Map — Determinants

    June 10, 2025
    Our Picks

    Dispatch: Partying at one of Africa’s largest AI gatherings

    October 22, 2025

    Topp 10 AI-filmer genom tiderna

    October 22, 2025

    OpenAIs nya webbläsare ChatGPT Atlas

    October 22, 2025
    Categories
    • AI Technology
    • AI Tools & Technologies
    • Artificial Intelligence
    • Latest AI Innovations
    • Latest News
    • Privacy Policy
    • Disclaimer
    • Terms and Conditions
    • About us
    • Contact us
    Copyright © 2025 ProfitlyAI All Rights Reserved.

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