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:

  • Node ID Tracking: Each node must have a unique identifier. The populationId helps our NodeTracker maintain separate node ID counters for different populations.
  • Innovation Tracking: Innovation numbers are historical markings that identify when and where structural changes (like new connections) first occurred in a population's evolutionary history.
  • Consistent Crossover: When genomes mate, they need to identify which genes correspond to one another across different genomes. This requires a shared innovation numbering system.
  • Separation for Multiple Experiments: Using different population IDs allows you to run multiple separate NEAT experiments in the same application without their innovation and node systems interfering with each other.

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.

ParameterTypeDescription
configConfigConfiguration object containing parameters for genome creation
populationIdString/NumberID of the population this genome belongs to
ReturnsTypeDescription
genomeGenomeA new genome with the specified topology
javascript
// 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.

javascript
// 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:

javascript
// 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

GenomeBuilderManual Creation
Simple and quick to useMore complex but offers greater control
Good for initializing populationsGood for specific network architectures
Creates fully-connected input-to-output networksCan create any connection pattern
Handles innovation tracking automaticallyRequires manual innovation tracking
Limited to config-based topologiesCan implement custom topologies