Creating Custom Genomes

This documentation explains how to create custom genomes in NEAT (NeuroEvolution of Augmenting Topologies). A genome in NEAT represents the genetic encoding of a neural network, containing nodes (neurons) and connections between them, along with their associated weights and properties.

There are two primary methods for creating genomes in NEAT. You can either use the GenomeBuilder utility class to generate genomes based on configuration parameters, or you can manually construct genomes by creating and connecting individual nodes.

Understanding Population ID

The populationID parameter is a feature specific to this NEAT implementation that ensures proper tracking of nodes and innovations within a population of genomes. While not part of the original NEAT algorithm, in our implementation it's important for the following reasons:

Important

All genomes within the same population must share the same populationID. This ensures that when structural mutations occur, innovation numbers are assigned consistently across the entire population.

Method 1: Using GenomeBuilder

The GenomeBuilder class provides a convenient way to create genomes with a standard topology determined by your configuration settings. This approach is recommended for initializing populations with consistent starting topologies.

buildGenome

GenomeBuilder.buildGenome(config, populationID) → Genome

Creates a new genome with input and output nodes connected according to the configuration parameters.

Parameter Type Description
config Config Configuration object containing parameters for genome creation
populationID String/Number ID of the population this genome belongs to
Returns Type Description
genome Genome A new genome with the specified topology
// First, create a configuration object const config = new Config({ inputSize: 3, // Number of input nodes outputSize: 2, // Number of output nodes useBias: true, // Whether to use a bias node connectBias: true, // Connect bias to output nodes activationFunction: 'Sigmoid', weightInitialization: { type: 'Random', params: [-1, 1] // Weight range } // ... other config parameters }); // Create a unique ID for the population // All genomes within the same evolving population MUST share this ID const populationID = "myPopulation1"; // Build a new genome const genome = GenomeBuilder.buildGenome(config, populationID); // When creating multiple genomes for the same population: const genome2 = GenomeBuilder.buildGenome(config, populationID); const genome3 = GenomeBuilder.buildGenome(config, populationID); // These all share innovation numbering system because they use the same populationID

Note

When using GenomeBuilder, the InnovationTracker (obtained through StaticManager) automatically handles the tracking of innovation numbers and node IDs for you using the provided populationID. This ensures that when mutations occur later, the historical markings are properly maintained within that population.

The system maintains separate innovation trackers for each unique population ID, which is why it's essential to use the same populationID for all genomes that will be evolved together.

Method 2: Manual Genome Creation

For more control over the genome structure, you can manually create nodes and connections. This approach is useful when you want to design specific neural network architectures.

To manually create a genome, you need to follow these steps in the correct order:

  1. Create input nodes first
  2. Create output nodes second (the order is important for proper ID assignment)
  3. Create bias nodes and hidden nodes
  4. Create connections between nodes
  5. Construct a genome from these components

Note on Node Order

It is essential to initialize nodes in the correct order: inputs first, then outputs, followed by bias and hidden nodes. This sequence is required due to how node IDs are managed internally.

// Import required components const { Genome, InputNode, OutputNode, BiasNode, HiddenNode, ConnectionGene, StaticManager } = require('neat-javascript'); // Create a configuration const config = new Config({ activationFunction: 'Sigmoid', // ... other parameters }); // Set the population ID const populationID = "customGenome1"; // Step 1: Create input nodes FIRST const input1 = new InputNode(StaticManager.getNodeTracker(populationID).getNextNodeID(), config); const input2 = new InputNode(StaticManager.getNodeTracker(populationID).getNextNodeID(), config); // Step 2: Create output nodes SECOND const output = new OutputNode(StaticManager.getNodeTracker(populationID).getNextNodeID(), config); // Step 3: Create bias and hidden nodes const biasNode = new BiasNode(StaticManager.getNodeTracker(populationID).getNextNodeID(), config); const hidden1 = new HiddenNode(StaticManager.getNodeTracker(populationID).getNextNodeID(), config); // Step 4: Create connections with tracked innovation numbers const connections = []; // Connection from input1 to hidden1 // First get the innovation tracker for this population const innovationTracker = StaticManager.getInnovationTracker(populationID); // Then track the innovation to get its unique ID let innovationData = innovationTracker.trackInnovation(input1.id, hidden1.id); connections.push( new ConnectionGene( input1, // Input node hidden1, // Output node config.weightInitialization.initializeWeight(), // Weight true, // Enabled innovationData.innovationNumber, // Innovation number false, // Recurrent config // Config ) ); // Connection from input2 to hidden1 innovationData = StaticManager.getInnovationTracker(populationID) .trackInnovation(input2.id, hidden1.id); connections.push( new ConnectionGene( input2, hidden1, config.weightInitialization.initializeWeight(), true, innovationData.innovationNumber, false, config ) ); // Connection from hidden1 to output innovationData = StaticManager.getInnovationTracker(populationID) .trackInnovation(hidden1.id, output.id); connections.push( new ConnectionGene( hidden1, output, config.weightInitialization.initializeWeight(), true, innovationData.innovationNumber, false, config ) ); // Connection from bias to hidden1 innovationData = StaticManager.getInnovationTracker(populationID) .trackInnovation(biasNode.id, hidden1.id); connections.push( new ConnectionGene( biasNode, hidden1, config.weightInitialization.initializeWeight(), true, innovationData.innovationNumber, false, config ) ); // Step 5: Create the genome const genome = new Genome( [input1, input2, output, biasNode, hidden1], // All node genes in recommended order connections, // All connection genes config, // Configuration populationID // Population ID );

Important

When manually creating genomes, it's crucial to properly track innovation numbers using the InnovationTracker (obtained via StaticManager) with the correct populationID. This ensures that when multiple genomes are evolved together in a population, their historical markings are consistent, which is essential for crossover operations and speciation.

If you create multiple custom genomes for the same population, always use the same populationID when calling StaticManager.getNodeTracker(populationID) and StaticManager.getInnovationTracker(populationID). This allows the system to properly track which innovations and node IDs have already been created within that population.

Creating Recurrent Connections

If you want to create recurrent connections (connections that point backwards in the network), set the "recurrent" parameter to true when creating the ConnectionGene:

// Creating a recurrent connection from hidden1 back to itself innovationData = StaticManager.getInnovationTracker(populationID) .trackInnovation(hidden1.id, hidden1.id); connections.push( new ConnectionGene( hidden1, // Input node hidden1, // Same node as output (recurrent) config.weightInitialization.initializeWeight(), // Weight true, // Enabled innovationData.innovationNumber, // Innovation number true, // Recurrent = true config // Config ) );

Comparison of Methods

GenomeBuilder Manual Creation
Simple and quick to use More complex but offers greater control
Good for initializing populations Good for specific network architectures
Creates fully-connected input-to-output networks Can create any connection pattern
Handles innovation tracking automatically Requires manual innovation tracking
Limited to config-based topologies Can implement custom topologies