
Day 33: Case Study: Using a Genetic Algorithms to Optimize Hyperparameters in a Neural Network
- Chris Woodruff
- September 16, 2025
- Genetic Algorithms
- .NET, ai, C#, dotnet, genetic algorithms, programming
- 0 Comments
Tuning hyperparameters for machine learning models like neural networks can be tedious and time-consuming. Traditional grid search or random search lacks efficiency in high-dimensional or non-linear search spaces. Genetic Algorithms (GAs) offer a compelling alternative by navigating the hyperparameter space with adaptive and evolutionary pressure. In this post, we’ll walk through using a Genetic Algorithm in C# to optimize neural network hyperparameters using a practical example.
The Optimization Problem
Let’s say you have a simple feedforward neural network built with ML.NET or a custom framework. Your goal is to find the best combination of the following hyperparameters:
- Number of hidden layers (1 to 3)
- Neurons per layer (10 to 100)
- Learning rate (0.0001 to 0.1)
- Batch size (16 to 256)
Each combination impacts the training accuracy and performance of the model. Using a GA, we encode each hyperparameter into a gene and build chromosomes representing complete configurations.
Chromosome Design
We can define a chromosome as a list of parameters:
public class HyperparameterChromosome { public int HiddenLayers { get; set; } // [1-3] public int NeuronsPerLayer { get; set; } // [10-100] public double LearningRate { get; set; } // [0.0001 - 0.1] public int BatchSize { get; set; } // [16 - 256] public double Fitness { get; set; } }
A random generator function initializes a diverse population:
public static HyperparameterChromosome GenerateRandomChromosome() { Random rand = new(); return new HyperparameterChromosome { HiddenLayers = rand.Next(1, 4), NeuronsPerLayer = rand.Next(10, 101), LearningRate = Math.Round(rand.NextDouble() * (0.1 - 0.0001) + 0.0001, 5), BatchSize = rand.Next(16, 257) }; }
Fitness Function: Model Evaluation
We define fitness based on validation accuracy from training the neural network using the encoded hyperparameters. To save time during this example, we simulate the evaluation step with a mocked accuracy function:
public static double EvaluateChromosome(HyperparameterChromosome chromo) { // Simulate evaluation for example purposes double accuracy = 0.6 + (0.1 * (chromo.HiddenLayers - 1)) + (0.001 * chromo.NeuronsPerLayer) - Math.Abs(chromo.LearningRate - 0.01) + (0.0001 * chromo.BatchSize); chromo.Fitness = accuracy; return accuracy; }
In a real-world case, you’d integrate this with a training loop using a machine learning library and compute validation accuracy.
Genetic Operators
Use standard crossover and mutation mechanisms. For simplicity, we’ll implement single-point crossover and bounded random mutation:
public static (HyperparameterChromosome, HyperparameterChromosome) Crossover(HyperparameterChromosome p1, HyperparameterChromosome p2) { return ( new HyperparameterChromosome { HiddenLayers = p1.HiddenLayers, NeuronsPerLayer = p2.NeuronsPerLayer, LearningRate = p1.LearningRate, BatchSize = p2.BatchSize }, new HyperparameterChromosome { HiddenLayers = p2.HiddenLayers, NeuronsPerLayer = p1.NeuronsPerLayer, LearningRate = p2.LearningRate, BatchSize = p1.BatchSize } ); } public static void Mutate(HyperparameterChromosome chromo) { Random rand = new(); int geneToMutate = rand.Next(0, 4); switch (geneToMutate) { case 0: chromo.HiddenLayers = rand.Next(1, 4); break; case 1: chromo.NeuronsPerLayer = rand.Next(10, 101); break; case 2: chromo.LearningRate = Math.Round(rand.NextDouble() * (0.1 - 0.0001) + 0.0001, 5); break; case 3: chromo.BatchSize = rand.Next(16, 257); break; } }
GA Loop
The core loop evaluates the population, selects parents, applies crossover and mutation, and evolves across generations.
int populationSize = 50; int generations = 30; double mutationRate = 0.1; List<HyperparameterChromosome> population = Enumerable.Range(0, populationSize) .Select(_ => GenerateRandomChromosome()) .ToList(); for (int gen = 0; gen < generations; gen++) { foreach (var chromo in population) EvaluateChromosome(chromo); population = population.OrderByDescending(c => c.Fitness).ToList(); List<HyperparameterChromosome> newPopulation = new List<HyperparameterChromosome>(); while (newPopulation.Count < populationSize) { var parent1 = TournamentSelect(population); var parent2 = TournamentSelect(population); var (child1, child2) = Crossover(parent1, parent2); if (new Random().NextDouble() < mutationRate) Mutate(child1); if (new Random().NextDouble() < mutationRate) Mutate(child2); newPopulation.Add(child1); newPopulation.Add(child2); } population = newPopulation; Console.WriteLine($"Gen {gen}: Best Accuracy = {population[0].Fitness:F4}"); }
Tournament Selection
A common selection method that balances performance and diversity:
public static HyperparameterChromosome TournamentSelect(List<HyperparameterChromosome> population, int tournamentSize = 5) { Random rand = new(); var competitors = population.OrderBy(_ => rand.Next()).Take(tournamentSize).ToList(); return competitors.OrderByDescending(c => c.Fitness).First(); }
Results and Use Cases
When integrated with real neural network training, this GA framework can intelligently explore the hyperparameter landscape. It adapts based on feedback and converges toward high-performing configurations over time.
This approach is especially useful when:
- The search space is non-continuous or complex
- Training time is expensive and gradient-free optimization is preferred
- Combinatorial parameter dependencies exist
GAs will not replace traditional optimizers for every use case, but they can offer robustness and simplicity when tuning difficult or poorly-behaved models.
In the next post, we will look at integrating GA-based hyperparameter tuning into an ML pipeline using automated tools and reporting.