Welcome to the domain of Multi-Objective Optimization. This is a field I wrote a dissertation on. There are numerous algorithms for solving problems of this kind, but perhaps the two most-famous are NSGA-II and SPEA2.
Of course, you only have a single-objective: whatever it is your scoring function is yielding. I'd submit that multi-objective algorithms would also apply, because you're interested in not just a single solution, but populations of them.
Might I point you to http://jmetalnet.sourceforge.net/?
The idea is that you will be generating populations of random vectors containing inputs that range across your domain of 100^6 possible solutions. Those populations will be mutated and mated with each other to generate new solutions, and from those new populations, they are down-selected in such a way that the more preferred solutions are elected to remain (and survive the evolution).
In the multi-world, you might have challenges comparing the fitness of different solutions. But in your single-objective world, comparing fitnesses are easy: you just need to decide whether you want higher numbers or lower. It seems like you want higher.
Outlined
- Create random population of solutions.
- Randomly mutate/crossover across your solutions.
- Calculate fitness of everyone, and sort.
- Down-sample back to the initial population size of best solutions.
- Repeat Steps2-4 [until convergence: until avg fitness > threshold?]
- Output final generation.
Results:
Here's a poor analysis, and to note, you could do much better by averaging result of (say) 20 runs at each parameter level. Right off the bat, you can tell that mutation rate should stay low, and obviously, a higher population size can help (to a converging point).
Results formatted as average score before,after, with a max of 600
PopSize=100,NumGens=50,MutRate=0.2,CrossRate=0.8
295.23,542.12
PopSize=100,NumGens=500,MutRate=0.2,CrossRate=0.8
298.53,565
PopSize=1000,NumGens=50,MutRate=0.2,CrossRate=0.8
301.814,579.334
PopSize=10000,NumGens=500,MutRate=0.2,CrossRate=0.8
299.8901,588
PopSize=1000,NumGens=50,MutRate=0.4,CrossRate=0.8
306.22,385.55
Code
I wrote this code in like 20 minutes, so it's not meant to be elegant or awesome. I hope it just gets the point across.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
namespace moo_in_csharp
{
internal class Individual
{
public int[] Decisions;
public double Fitness;
private int _numdecisions = 6;
/// <summary>
/// Default constructor.
/// </summary>
public Individual()
{
Decisions = new int[_numdecisions];
}
/// <summary>
/// Replaces first half of decisions with those of the mate.
/// </summary>
/// <param name="mate"></param>
public void Crossover(Individual mate)
{
int crossoverPoint = _numdecisions / 2;
for (int i = 0; i < crossoverPoint; i++)
{
Decisions[i] = mate.Decisions[i];
}
}
/// <summary>
/// Simple fitness function that computes sum of a+b+c+d+e+f.
/// </summary>
public double Evaluate()
{
Fitness = Decisions.Sum();
return Fitness;
}
/// <summary>
/// Assigns random values to its decisions.
/// </summary>
public void Generate()
{
for (int i = 0; i < _numdecisions; i++)
{
Decisions[i] = Program.rand.Next(0, 101);
}
}
/// <summary>
/// Randomly mutate select decisions.
/// </summary>
public void Mutate()
{
for (int i = 0; i < _numdecisions; i++)
{
Decisions[i] = Program.rand.Next(0, 101);
}
}
}
internal class Program
{
public static Random rand = new Random();
private static void Main(string[] args)
{
//parameters
int populationSize = 100;
int numGenerations = 50;
double mutationRate = 0.2;
double crossoverRate = 0.8;
//build initial population
List<Individual> solutions = new List<Individual>();
for (int i = 0; i < populationSize; i++)
{
var solution = new Individual();
solution.Generate();
solution.Evaluate();
solutions.Add(solution);
}
//calculate average score of initial population
var averageScoreBefore = solutions.Select(x => x.Evaluate()).Average();
//iterate across number of generations
for (int i = 0; i < numGenerations; i++)
{
//build offspring by mating sequential pairs of solutions
var offspring = new List<Individual>();
for (int e = 0; e < solutions.Count() - 1; e += 2)
{
if (rand.NextDouble() < crossoverRate)
{
var newIndividual = new Individual();
solutions[e].Decisions.CopyTo(newIndividual.Decisions, 0);
newIndividual.Crossover(solutions[e + 1]);
offspring.Add(newIndividual);
}
}
//add our offspring to our solutions
solutions.AddRange(offspring);
//mutate solutions at a low rate
foreach (var solution in solutions)
{
if (rand.NextDouble() < mutationRate)
{
solution.Mutate();
}
}
//sort our solutions and down-sample to initial population size
solutions = solutions.OrderByDescending(x => x.Evaluate()).ToList();
solutions = solutions.Take(populationSize).ToList();
}
//calculate average score after
var averageScoreAfter = solutions.Select(x => x.Evaluate()).Average();
Debug.WriteLine(averageScoreBefore + "," + averageScoreAfter);
}
}
}
Other Notes
Your runtime will mostly be dependent on your fitness scoring function. For simple math functions, this runtime won't be hard. Obviously, if there's a process involved instead, you'd want to keep the number of evaluations to a minimum. This is what I studied for my Ph.D., and developed a new algorithm entitled GALE: