The Neuroevolution module provides implementations of evolutionary algorithms for optimizing neural network architectures and weights. Neuroevolution combines principles from evolutionary computation with neural networks, enabling the automatic discovery of effective network topologies and parameters without relying on gradient-based optimization.
This approach is particularly valuable for problems where gradient information is unavailable or unreliable, such as reinforcement learning tasks with sparse rewards, or when optimizing complex network architectures. The module implements various neuroevolution algorithms, including NEAT (NeuroEvolution of Augmenting Topologies), HyperNEAT, and evolutionary strategies like CMA-ES (Covariance Matrix Adaptation Evolution Strategy).
Genetic algorithms are population-based optimization methods inspired by natural selection:
NEAT is an algorithm that evolves both the weights and structure of neural networks:
HyperNEAT extends NEAT to evolve large-scale networks with geometric regularities:
Evolutionary strategies focus on optimizing network weights:
Genetic programming evolves programs or expressions:
neurenix.neuroevolution.GeneticAlgorithm(
population_size: int = 100,
mutation_rate: float = 0.1,
crossover_rate: float = 0.7,
selection_method: str = "tournament",
elitism: int = 2
)
Creates a genetic algorithm for optimizing neural networks.
Parameters:
- population_size: Number of individuals in the population
- mutation_rate: Probability of mutation
- crossover_rate: Probability of crossover
- selection_method: Method for selecting individuals ("tournament", "roulette", "rank")
- elitism: Number of best individuals to preserve unchanged
Methods:
- initialize(genome_factory): Initialize the population
- evolve(fitness_function, generations): Evolve the population
- get_best_individual(): Get the best individual in the population
- get_population(): Get the current population
Example:
from neurenix.neuroevolution import GeneticAlgorithm
from neurenix.nn import Sequential, Linear, ReLU
# Define a function to create neural networks
def create_network():
return Sequential(
Linear(10, 20),
ReLU(),
Linear(20, 5)
)
# Define a fitness function
def evaluate_network(network):
# Evaluate the network on some task
# Return a fitness score
return fitness_score
# Create a genetic algorithm
ga = GeneticAlgorithm(
population_size=50,
mutation_rate=0.1,
crossover_rate=0.7,
selection_method="tournament",
elitism=2
)
# Initialize the population
ga.initialize(create_network)
# Evolve the population
best_network = ga.evolve(evaluate_network, generations=100)
# Get the best network
best_network = ga.get_best_individual()
neurenix.neuroevolution.NEAT(
input_size: int,
output_size: int,
population_size: int = 100,
compatibility_threshold: float = 3.0,
compatibility_disjoint_coefficient: float = 1.0,
compatibility_weight_coefficient: float = 0.4,
survival_threshold: float = 0.2
)
Creates a NEAT algorithm for evolving neural network topologies and weights.
Parameters:
- input_size: Number of input neurons
- output_size: Number of output neurons
- population_size: Number of individuals in the population
- compatibility_threshold: Threshold for speciation
- compatibility_disjoint_coefficient: Coefficient for disjoint genes in compatibility calculation
- compatibility_weight_coefficient: Coefficient for weight differences in compatibility calculation
- survival_threshold: Fraction of each species allowed to reproduce
Methods:
- initialize(): Initialize the population
- evolve(fitness_function, generations): Evolve the population
- get_best_individual(): Get the best individual in the population
- get_species(): Get the current species
Example:
from neurenix.neuroevolution import NEAT
# Define a fitness function
def evaluate_network(network):
# Evaluate the network on some task
# Return a fitness score
return fitness_score
# Create a NEAT algorithm
neat = NEAT(
input_size=10,
output_size=5,
population_size=100,
compatibility_threshold=3.0
)
# Initialize the population
neat.initialize()
# Evolve the population
neat.evolve(evaluate_network, generations=100)
# Get the best network
best_network = neat.get_best_individual()
neurenix.neuroevolution.HyperNEAT(
substrate_dimensions: List[Tuple[int, int]],
cppn_output_functions: List[str],
population_size: int = 100,
compatibility_threshold: float = 3.0
)
Creates a HyperNEAT algorithm for evolving large-scale neural networks with geometric regularities.
Parameters:
- substrate_dimensions: Dimensions of each substrate layer
- cppn_output_functions: Activation functions for CPPN outputs
- population_size: Number of individuals in the population
- compatibility_threshold: Threshold for speciation
Methods:
- initialize(): Initialize the population
- evolve(fitness_function, generations): Evolve the population
- get_best_individual(): Get the best individual in the population
- get_substrate_network(cppn): Generate a substrate network from a CPPN
Example:
from neurenix.neuroevolution import HyperNEAT
# Define a fitness function
def evaluate_network(network):
# Evaluate the network on some task
# Return a fitness score
return fitness_score
# Create a HyperNEAT algorithm
hyperneat = HyperNEAT(
substrate_dimensions=[(10, 1), (20, 1), (5, 1)], # Input, hidden, output layers
cppn_output_functions=["sigmoid", "tanh"],
population_size=100
)
# Initialize the population
hyperneat.initialize()
# Evolve the population
hyperneat.evolve(evaluate_network, generations=100)
# Get the best CPPN
best_cppn = hyperneat.get_best_individual()
# Generate a substrate network from the CPPN
substrate_network = hyperneat.get_substrate_network(best_cppn)
neurenix.neuroevolution.CMAES(
model: neurenix.nn.Module,
population_size: int = 16,
sigma: float = 0.1,
learning_rate: float = 0.1
)
Creates a CMA-ES algorithm for optimizing neural network weights.
Parameters:
- model: Neural network model to optimize
- population_size: Number of individuals in the population
- sigma: Initial step size
- learning_rate: Learning rate for updating the distribution
Methods:
- initialize(): Initialize the algorithm
- evolve(fitness_function, generations): Evolve the population
- get_best_individual(): Get the best individual in the population
- get_distribution_parameters(): Get the current distribution parameters
Example:
from neurenix.neuroevolution import CMAES
from neurenix.nn import Sequential, Linear, ReLU
# Create a neural network
model = Sequential(
Linear(10, 20),
ReLU(),
Linear(20, 5)
)
# Define a fitness function
def evaluate_network(network):
# Evaluate the network on some task
# Return a fitness score
return fitness_score
# Create a CMA-ES algorithm
cmaes = CMAES(
model=model,
population_size=16,
sigma=0.1
)
# Initialize the algorithm
cmaes.initialize()
# Evolve the population
cmaes.evolve(evaluate_network, generations=100)
# Get the best network
best_network = cmaes.get_best_individual()
neurenix.neuroevolution.Population(
size: int,
genome_factory: Callable,
fitness_function: Callable
)
Creates a population of individuals for evolutionary algorithms.
Parameters:
- size: Number of individuals in the population
- genome_factory: Function to create new genomes
- fitness_function: Function to evaluate genomes
Methods:
- initialize(): Initialize the population
- evaluate(): Evaluate all individuals in the population
- select(method, count): Select individuals for reproduction
- crossover(parent1, parent2): Perform crossover between two parents
- mutate(individual, rate): Mutate an individual
- get_best_individual(): Get the best individual in the population
- get_average_fitness(): Get the average fitness of the population
Example:
from neurenix.neuroevolution import Population
# Define a function to create genomes
def create_genome():
# Create a new genome
return genome
# Define a fitness function
def evaluate_genome(genome):
# Evaluate the genome
return fitness_score
# Create a population
population = Population(
size=100,
genome_factory=create_genome,
fitness_function=evaluate_genome
)
# Initialize the population
population.initialize()
# Evaluate the population
population.evaluate()
# Select individuals for reproduction
parents = population.select(method="tournament", count=10)
# Perform crossover
child = population.crossover(parents[0], parents[1])
# Mutate the child
mutated_child = population.mutate(child, rate=0.1)
# Get the best individual
best_individual = population.get_best_individual()
neurenix.neuroevolution.Genome(
genes: List[Any] = None,
fitness: float = 0.0
)
Represents a genome for evolutionary algorithms.
Parameters:
- genes: List of genes
- fitness: Fitness value
Methods:
- crossover(other): Perform crossover with another genome
- mutate(rate): Mutate the genome
- distance(other): Calculate distance to another genome
- copy(): Create a copy of the genome
- to_network(): Convert the genome to a neural network
Example:
from neurenix.neuroevolution import Genome
# Create a genome
genome1 = Genome(genes=[1, 2, 3, 4, 5])
genome2 = Genome(genes=[5, 4, 3, 2, 1])
# Perform crossover
child = genome1.crossover(genome2)
# Mutate the child
child.mutate(rate=0.1)
# Calculate distance between genomes
distance = genome1.distance(genome2)
# Convert to a neural network
network = genome1.to_network()
neurenix.neuroevolution.Species(
representative: Genome,
compatibility_threshold: float = 3.0
)
Represents a species in the NEAT algorithm.
Parameters:
- representative: Representative genome for the species
- compatibility_threshold: Threshold for determining membership
Methods:
- add(genome): Add a genome to the species
- remove(genome): Remove a genome from the species
- contains(genome): Check if a genome belongs to the species
- get_adjusted_fitness(): Get the adjusted fitness of the species
- select_representative(): Select a new representative for the species
- cull(percentage): Remove the worst individuals from the species
Example:
from neurenix.neuroevolution import Species, Genome
# Create a representative genome
representative = Genome(genes=[1, 2, 3, 4, 5])
# Create a species
species = Species(
representative=representative,
compatibility_threshold=3.0
)
# Create another genome
genome = Genome(genes=[1, 2, 4, 5, 6])
# Check if the genome belongs to the species
if species.contains(genome):
# Add the genome to the species
species.add(genome)
# Get the adjusted fitness of the species
adjusted_fitness = species.get_adjusted_fitness()
# Select a new representative
species.select_representative()
# Cull the worst individuals
species.cull(percentage=0.5)
neurenix.neuroevolution.GeneticProgramming(
function_set: List[Callable],
terminal_set: List[Any],
population_size: int = 100,
max_depth: int = 6,
method: str = "tree"
)
Creates a genetic programming algorithm for evolving programs or expressions.
Parameters:
- function_set: Set of functions to use in the programs
- terminal_set: Set of terminals (variables, constants) to use in the programs
- population_size: Number of individuals in the population
- max_depth: Maximum depth of the program trees
- method: Method for representing programs ("tree", "cartesian", "grammar")
Methods:
- initialize(): Initialize the population
- evolve(fitness_function, generations): Evolve the population
- get_best_program(): Get the best program in the population
- evaluate_program(program, inputs): Evaluate a program on inputs
Example:
import numpy as np
from neurenix.neuroevolution import GeneticProgramming
# Define function set
def add(x, y): return x + y
def sub(x, y): return x - y
def mul(x, y): return x * y
def div(x, y): return x / y if y != 0 else 1
function_set = [add, sub, mul, div]
terminal_set = ["x", "y", 1, 2, 3]
# Define fitness function
def evaluate_program(program, target_function):
errors = []
for x in np.linspace(-1, 1, 20):
for y in np.linspace(-1, 1, 20):
expected = target_function(x, y)
actual = program({"x": x, "y": y})
errors.append((expected - actual) ** 2)
return -np.mean(errors) # Negative because we maximize fitness
# Target function to approximate
def target_function(x, y):
return x**2 + y**2
# Create a genetic programming algorithm
gp = GeneticProgramming(
function_set=function_set,
terminal_set=terminal_set,
population_size=100,
max_depth=6,
method="tree"
)
# Initialize the population
gp.initialize()
# Evolve the population
gp.evolve(lambda p: evaluate_program(p, target_function), generations=100)
# Get the best program
best_program = gp.get_best_program()
| Feature | Neurenix | TensorFlow |
|---|---|---|
| Neuroevolution Support | Comprehensive (NEAT, HyperNEAT, CMA-ES) | Limited (basic genetic algorithms) |
| Topology Evolution | Native support | No native support |
| Integration with NN API | Seamless | Requires custom implementation |
| Speciation | Built-in | Not available |
| Distributed Evolution | Supported | Limited support |
| Visualization Tools | Comprehensive | Limited |
Neurenix provides more comprehensive neuroevolution capabilities compared to TensorFlow, with built-in support for advanced algorithms like NEAT and HyperNEAT. TensorFlow requires custom implementations for most neuroevolution features, making it less accessible for users interested in evolutionary approaches to neural network optimization.
| Feature | Neurenix | PyTorch |
|---|---|---|
| Neuroevolution Support | Comprehensive (NEAT, HyperNEAT, CMA-ES) | No native support |
| Topology Evolution | Native support | Requires third-party libraries |
| Integration with NN API | Seamless | Requires custom implementation |
| Speciation | Built-in | Not available natively |
| Distributed Evolution | Supported | Requires custom implementation |
| Visualization Tools | Comprehensive | Limited |
PyTorch does not provide native support for neuroevolution, requiring users to rely on third-party libraries or custom implementations. Neurenix's integrated neuroevolution module offers a more cohesive experience, with seamless integration with the rest of the framework and built-in support for various evolutionary algorithms.
| Feature | Neurenix | Scikit-Learn |
|---|---|---|
| Neuroevolution Support | Comprehensive | No support |
| Neural Network Support | Native | Limited |
| Evolutionary Algorithms | Specialized for NN | Generic genetic algorithms |
| Topology Evolution | Supported | Not supported |
| Integration with Deep Learning | Seamless | Limited |
| Visualization Tools | Comprehensive | Limited |
Scikit-Learn provides some basic genetic algorithm implementations but lacks support for neuroevolution and topology evolution. Neurenix fills this gap with its comprehensive neuroevolution module, enabling the evolution of both network weights and topologies for a wide range of applications.
Choose the appropriate neuroevolution algorithm based on your task:
from neurenix.neuroevolution import NEAT, HyperNEAT, CMAES
# For tasks requiring topology evolution
neat = NEAT(
input_size=10,
output_size=5,
population_size=100
)
# For tasks with geometric regularities
hyperneat = HyperNEAT(
substrate_dimensions=[(10, 1), (20, 1), (5, 1)],
cppn_output_functions=["sigmoid", "tanh"],
population_size=100
)
# For tasks where only weight optimization is needed
cmaes = CMAES(
model=model,
population_size=16,
sigma=0.1
)
Choose an appropriate population size based on the complexity of your task:
from neurenix.neuroevolution import NEAT
# For simple tasks
simple_neat = NEAT(
input_size=5,
output_size=2,
population_size=50 # Smaller population
)
# For complex tasks
complex_neat = NEAT(
input_size=20,
output_size=10,
population_size=200 # Larger population
)
Design effective fitness functions:
# Simple fitness function
def simple_fitness(network):
# Evaluate on a single task
return score
# Multi-objective fitness function
def multi_objective_fitness(network):
# Evaluate on multiple objectives
score1 = evaluate_objective1(network)
score2 = evaluate_objective2(network)
score3 = evaluate_objective3(network)
# Combine scores (weighted sum, Pareto dominance, etc.)
return 0.5 * score1 + 0.3 * score2 + 0.2 * score3
# Novelty-based fitness function
def novelty_fitness(network, archive):
# Calculate behavioral distance to archive
distances = [behavioral_distance(network, ind) for ind in archive]
# Return average distance to k-nearest neighbors
return sum(sorted(distances)[:15]) / 15
Tune hyperparameters for better performance:
from neurenix.neuroevolution import NEAT
# Start with default parameters
neat = NEAT(
input_size=10,
output_size=5,
population_size=100,
compatibility_threshold=3.0,
compatibility_disjoint_coefficient=1.0,
compatibility_weight_coefficient=0.4,
survival_threshold=0.2
)
# Adjust parameters based on observations
# If species are too fragmented:
neat = NEAT(
input_size=10,
output_size=5,
population_size=100,
compatibility_threshold=4.0, # Increase threshold
compatibility_disjoint_coefficient=1.0,
compatibility_weight_coefficient=0.4,
survival_threshold=0.2
)
# If evolution is too slow:
neat = NEAT(
input_size=10,
output_size=5,
population_size=100,
compatibility_threshold=3.0,
compatibility_disjoint_coefficient=1.0,
compatibility_weight_coefficient=0.4,
survival_threshold=0.3 # Increase survival rate
)
import neurenix as nx
from neurenix.neuroevolution import NEAT
import numpy as np
# Define a simple XOR problem
inputs = np.array([[0, 0], [0, 1], [1, 0], [1, 1]])
outputs = np.array([[0], [1], [1], [0]])
# Define a fitness function
def evaluate_network(network):
total_error = 0
for i in range(len(inputs)):
input_tensor = nx.Tensor(inputs[i])
output = network(input_tensor).item()
target = outputs[i][0]
total_error += (output - target) ** 2
# Convert error to fitness (higher is better)
fitness = 4.0 - total_error
return max(0.0, fitness) # Ensure non-negative fitness
# Create a NEAT algorithm
neat = NEAT(
input_size=2,
output_size=1,
population_size=150,
compatibility_threshold=3.0
)
# Initialize the population
neat.initialize()
# Evolve the population
best_fitness_history = []
for generation in range(100):
# Evolve one generation
neat.evolve(evaluate_network, generations=1)
# Get the best individual
best_individual = neat.get_best_individual()
best_fitness = evaluate_network(best_individual)
best_fitness_history.append(best_fitness)
# Print progress
print(f"Generation {generation}: Best Fitness = {best_fitness}")
# Check if we've solved the problem
if best_fitness > 3.9: # Close enough to perfect
print("Solution found!")
break
# Test the best network
best_network = neat.get_best_individual()
print("\nTesting the best network:")
for i in range(len(inputs)):
input_tensor = nx.Tensor(inputs[i])
output = best_network(input_tensor).item()
print(f"Input: {inputs[i]}, Output: {output:.4f}, Target: {outputs[i][0]}")
# Plot the fitness history
import matplotlib.pyplot as plt
plt.figure(figsize=(10, 6))
plt.plot(best_fitness_history)
plt.title("Best Fitness over Generations")
plt.xlabel("Generation")
plt.ylabel("Fitness")
plt.grid(True)
plt.savefig("neat_fitness_history.png")
import neurenix as nx
from neurenix.nn import Sequential, Linear, ReLU
from neurenix.neuroevolution import CMAES
import numpy as np
# Create a dataset
np.random.seed(42)
X = np.random.rand(100, 10)
y = np.sin(X.sum(axis=1)) + 0.1 * np.random.randn(100)
# Convert to tensors
X_tensor = nx.Tensor(X)
y_tensor = nx.Tensor(y).reshape(-1, 1)
# Create a neural network
model = Sequential(
Linear(10, 20),
ReLU(),
Linear(20, 20),
ReLU(),
Linear(20, 1)
)
# Define a fitness function
def evaluate_network(network):
# Forward pass
predictions = network(X_tensor)
# Calculate MSE
mse = ((predictions - y_tensor) ** 2).mean().item()
# Convert to fitness (higher is better)
return -mse # Negative because we maximize fitness
# Create a CMA-ES algorithm
cmaes = CMAES(
model=model,
population_size=16,
sigma=0.1
)
# Initialize the algorithm
cmaes.initialize()
# Evolve the population
best_fitness_history = []
for generation in range(100):
# Evolve one generation
cmaes.evolve(evaluate_network, generations=1)
# Get the best individual
best_individual = cmaes.get_best_individual()
best_fitness = evaluate_network(best_individual)
best_fitness_history.append(best_fitness)
# Print progress
print(f"Generation {generation}: Best Fitness = {best_fitness}")
# Check if we've reached a good solution
if best_fitness > -0.01: # MSE < 0.01
print("Good solution found!")
break
# Test the best network
best_network = cmaes.get_best_individual()
predictions = best_network(X_tensor).numpy().flatten()
# Plot the results
import matplotlib.pyplot as plt
plt.figure(figsize=(12, 10))
# Plot predictions vs targets
plt.subplot(2, 1, 1)
plt.scatter(y, predictions)
plt.plot([min(y), max(y)], [min(y), max(y)], 'r--')
plt.title("Predictions vs Targets")
plt.xlabel("Targets")
plt.ylabel("Predictions")
plt.grid(True)
# Plot fitness history
plt.subplot(2, 1, 2)
plt.plot([-f for f in best_fitness_history]) # Convert back to MSE
plt.title("MSE over Generations")
plt.xlabel("Generation")
plt.ylabel("Mean Squared Error")
plt.yscale('log')
plt.grid(True)
plt.tight_layout()
plt.savefig("cmaes_results.png")
import neurenix as nx
from neurenix.neuroevolution import HyperNEAT
import numpy as np
# Define a simple task: learning a 2D function
def target_function(x, y):
return np.sin(x * y) * np.cos(x + y)
# Generate a dataset
resolution = 16
x = np.linspace(-1, 1, resolution)
y = np.linspace(-1, 1, resolution)
X, Y = np.meshgrid(x, y)
Z = target_function(X, Y)
# Define a fitness function
def evaluate_network(network):
total_error = 0
for i in range(resolution):
for j in range(resolution):
# Create input coordinates
input_tensor = nx.Tensor([x[i], y[j]])
# Get network output
output = network(input_tensor).item()
# Calculate error
target = Z[j, i] # Note: meshgrid swaps indices
total_error += (output - target) ** 2
# Convert error to fitness (higher is better)
mean_error = total_error / (resolution * resolution)
fitness = 1.0 / (1.0 + mean_error)
return fitness
# Create a HyperNEAT algorithm
hyperneat = HyperNEAT(
substrate_dimensions=[(2, 1), (8, 8), (1, 1)], # Input, hidden, output layers
cppn_output_functions=["sigmoid", "tanh"],
population_size=100
)
# Initialize the population
hyperneat.initialize()
# Evolve the population
best_fitness_history = []
for generation in range(200):
# Evolve one generation
hyperneat.evolve(evaluate_network, generations=1)
# Get the best individual
best_cppn = hyperneat.get_best_individual()
best_network = hyperneat.get_substrate_network(best_cppn)
best_fitness = evaluate_network(best_network)
best_fitness_history.append(best_fitness)
# Print progress
print(f"Generation {generation}: Best Fitness = {best_fitness}")
# Check if we've reached a good solution
if best_fitness > 0.95: # Close enough to perfect
print("Good solution found!")
break
# Generate predictions from the best network
best_cppn = hyperneat.get_best_individual()
best_network = hyperneat.get_substrate_network(best_cppn)
predictions = np.zeros((resolution, resolution))
for i in range(resolution):
for j in range(resolution):
input_tensor = nx.Tensor([x[i], y[j]])
predictions[j, i] = best_network(input_tensor).item()
# Plot the results
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
plt.figure(figsize=(18, 6))
# Plot target function
ax1 = plt.subplot(1, 3, 1, projection='3d')
surf1 = ax1.plot_surface(X, Y, Z, cmap='viridis')
ax1.set_title("Target Function")
ax1.set_xlabel("X")
ax1.set_ylabel("Y")
ax1.set_zlabel("Z")
# Plot network output
ax2 = plt.subplot(1, 3, 2, projection='3d')
surf2 = ax2.plot_surface(X, Y, predictions, cmap='viridis')
ax2.set_title("Network Output")
ax2.set_xlabel("X")
ax2.set_ylabel("Y")
ax2.set_zlabel("Z")
# Plot fitness history
ax3 = plt.subplot(1, 3, 3)
ax3.plot(best_fitness_history)
ax3.set_title("Fitness over Generations")
ax3.set_xlabel("Generation")
ax3.set_ylabel("Fitness")
ax3.grid(True)
plt.tight_layout()
plt.savefig("hyperneat_results.png")
This documentation provides a comprehensive overview of the Neuroevolution module in Neurenix, including key concepts, API reference, framework comparisons, best practices, and tutorials for evolving neural networks using various evolutionary algorithms.