0

I have 6 int parameters ranging from 0 to 100

The total combination of the numbers are 100^6 and each combination gives a result ranging approx. from -10000 to 100000 or even more.

Input data example:
MySimulation (57, 78, 20, 10, 90, 50) = 300  <- Best Result
MySimulation (50, 80, 10, 90, 35, 8) = 200
MySimulation (4, 55, 40, 99, 40, 50) = -50 <- Worst Result

The higher the result the better the combination of numbers are, I already have the calculation which gives a result, I only need AI to find a better combination of numbers which gives a higher result.

Output data example:
55, 70, 25, 15, 95, 52   <- Let's say these combination of
                            numbers was choosen by AI and would 
                            give a result of 400 with my simulation

Note: The order of the numbers is also important.

How can I reduce the total combinations of 100^6 with AI, in order to get the best result without iterating through all 100^6 of combinations ?

I plan to use Accord.NET in C# (or is there something better?), an example of code would be helpful because I am new to AI.

Mario
  • 13,941
  • 20
  • 54
  • 110
  • Are you asking how to reverse engineer `Calculation` to find *the* optimal solution, or are you asking how to find a "pretty good" solution? You *could* use an AI framework to find a "pretty good" solution, but this is basically a local-maximum type problem, and additionally, has a small domain. Typical approach is (very roughly) to try random numbers, take the best results, and then try other combinations of nearby numbers and repeat until you're satisfied. – BurnsBA Oct 05 '17 at 12:15
  • I already have the calculation which gives a result, I just need a "pretty good" solution. – Mario Oct 05 '17 at 12:23
  • 1
    Without assumptions, you can't beat brute-force search. I don't see any assumptions / a-priori information in your post. – sascha Oct 05 '17 at 12:49
  • there are no assumptions because if you change a number it affects the other numbers – Mario Oct 07 '17 at 09:16
  • 2
    Do not use Accord.NET. This is not a machine learning problem. Have a look at genetic algorithms: https://stackoverflow.com/questions/14008/genetic-programming-in-c-sharp. Or simulated annealing. – MineR Oct 09 '17 at 02:43
  • 1
    accord.net also have GA and it's updated more recently than aforge – Mario Oct 09 '17 at 21:41
  • If you're interested in multi-objective optimization, take a look at my answer. Also, there's a package out there called jmetal that might be useful. – Joe Oct 13 '17 at 00:05
  • If you absolutely need to find the best optimum solution to your problem, then you cannot possibly beat brute-force search. You would really need to consider every possible combination, evaluate their result, and take the best value you've found. However, if you are willing to sacrifice a bit of performance for speed, then you might indeed want to take a look at the [Accord.Genetics](http://accord-framework.net/docs/html/N_Accord_Genetic.htm) namespace while using Accord.NET - it might provide you methods to search for a good enough solution without having to explore your entire search space. – Cesar Oct 23 '17 at 20:49

3 Answers3

3

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

  1. Create random population of solutions.
  2. Randomly mutate/crossover across your solutions.
  3. Calculate fitness of everyone, and sort.
  4. Down-sample back to the initial population size of best solutions.
  5. Repeat Steps2-4 [until convergence: until avg fitness > threshold?]
  6. 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:

Joe
  • 233
  • 1
  • 5
  • Thank you! I will try the code in my application and see where it gets. – Mario Oct 13 '17 at 12:15
  • Your sample uses NSGA-II, SPEA2 or GALE? – Mario Oct 13 '17 at 13:39
  • The code here isn't either of those. Here the case is more simple because you only have a single objective fitness score, so it's easier to draft a quick and dirty program. – Joe Oct 14 '17 at 01:18
  • I have implemented the code, it's just magic, in 5 minutes I got a 30% better fitness than the 64mil. brute force with lower resolution which took over 30mins. But I see that it does not always keep the highest score. After it reaches 470 then after a few generations the best solution score goes to 390, is this normal? – Mario Oct 14 '17 at 11:49
  • It's possible that good solutions are mutated into worse solutions at times. That's part of the magic of genetic algorithms. This code generates population, so you end up with a pool of good solutions. You could add a stopping criteria or you could set the mutation rate even lower. Also you can archive the best scoring solutions after every generation as a separate list if you like. – Joe Oct 15 '17 at 06:45
2

You could try to use a Metaheuristic / Stochastic Local Search algorithm to solve this type of problem as mentioned in many comments and the solution by BurnsCA.

Simulated Annealing and Genetic Algorithms are examples but there are many more. Whether this approach is feasible, depends on how quickly you can calculate changes in your objective function, i.e. evaluate the quality of a solution and changes in it.

If the output of your simulation has the property that minor changes dramatically changes the result, it may or may not be a better approach than brute forcing through some number of random assignments and taking the best one. You'll have to experiment.

Implementation of these algorithms themselves is usually not very complicated, and I think you can even use a library like NMath, e.g. take a look at

http://www.centerspace.net/doc/NMath/user/simulated-annealing-85273.htm

Your "objective function", the value that you're attempting to maximize (or minimize) is the output of the simulation.

Although the implementation of the algorithms themselves is not hard, efficient implementation of various aspects of them are.

What you need to do is to define a neighbourhood function, i.e. a way of getting from the solution (or state if you prefer) to another solution. In your case, this could involve the code example suggested by BurnsCA, which would be a 1-opt move, because you would randomly select another value for one parameter. You could also try 2-opt moves or more if 1-opt doesn't show improvements quickly enough.

The next thing you need is a way to evaluate the effect of taking a move. In other words, what is the difference in objective function between your current value and the value you will have if you take the move. If at all possible, you'll want a way to evaluate a move, without having to perform the entire simulation again every time.

The most naive approach (which is often called Descent) is to move to a neighbour solution at random, and if it found a better (higher objective function in your case) value, make that the new solution and then repeat that step until you can find no more improvements.

The problem with that approach is that you'll get stuck in a local maximum pretty quickly. Simulated annealing provides one method of attempting to avoid that, by not selecting only improvements, but selecting non-improving moves with a probability that depends on the current 'temperature' which is just a variable you decrease every iteration according to some annealing schedule you define.

Since the crux of implementing these methods is not in the overall algorithms themselves (although it does take a bit of time) but in the implementation of your neighbourhood and neighbourhood-evaluation functions, I personally don't think there's a lot of time saved by using some framework for that.

If this is a one time thing and the above is not feasible, you could also consider parallelizing the computations across thousands of machines to obtain an optimal solution. E.g. Azure Batch or similar services. Since you can test 50 mio combinations in 30 minutes (on one machine without parallelizing?), you could in principle provision 20 000 virtual machines and test all combinations in 30 minutes.

kkirk
  • 352
  • 1
  • 7
  • Thanks, but the problem is that the optimum solution must be found many times, not just once, for more products and also if I get better results with AI than the current brute force 60mil custom selected combinations I can add more parameters and improve the calculation even more. – Mario Oct 12 '17 at 20:41
  • You're welcome and in that case a (meta)heuristic such as Simulated Annealing (or similar) is probably worth a try. Just a detail I'd like to point out is, that no AI or local search algorithm will provide you with a guaranteed *globally optimal* solution, only *locally optimal* which may or may not be globally optimal (you won't know unless you solve the problem to optimality by e.g. brute-forcing). – kkirk Oct 12 '17 at 21:03
  • I know, but at least I can compare with the best result from the 60mil. brute force combination which I have right now and have decent results, so if I get something with at least 20-30% better result is good – Mario Oct 12 '17 at 21:55
1

You don't need a machine learning framework to implement a local optimization algorithm.

// Example linear calculation
public int Calculation(int a, int b, int c, int d, int e, int f)
{
    int result = 0;
    unchecked
    {
        result = (int)((double)a * Math.Tan((double)b * Math.PI / ((double)c + 0.001)));
        result += d + e + f;
    }

    return result;
}


var rand = new Random();

// The currently best known solution set
var best = new int[6] { 1, 1, 1, 1, 1, 1 };

// Score for best known solution set
int bestScore = int.MinValue;

// loop variables
int score;
var work = new int[6];

// Loop as appropriate.
for (int i=0; i<500; i++)
{
    // Copy over the best solution to modify it
    best.CopyTo(work, 0);

    // Change one of the parameters of the best solution
    work[rand.Next() % 6] = rand.Next() % 101;

    // Calculate new score with modified solution
    score = Calculation(work[0], work[1], work[2], work[3], work[4], work[5]);

    // Only keep this solution if it's better than anything else
    if (score > bestScore)
    {
        work.CopyTo(best, 0);
        bestScore = score;
    }
}

The above converges on a solution pretty quickly, mostly because the Calculation function is so friendly. After 500 iterations:

int[6] { 98, 1, 2, 98, 99, 100 }

Where an optimal solution would be { 100, 1, 2, 100, 100, 100 }

Note that this local-optimization approach only works for mostly linear problems.

Not shown, but you'd also want to see different starting values, or rerun the entire thing a bunch of times.

There's a nifty image on wikipedia page for hill climbing algorithms that kind of shows the essence of what this approach is trying to do.

BurnsBA
  • 4,347
  • 27
  • 39
  • That nifty image does not describe your algorithm (where the animation would look very different), but the slightly more complex Simulated-Annealing approach. – sascha Oct 05 '17 at 13:22
  • I don't know if this will work, this is just a random trial, it does not try numbers near the best results. Also please note that by changing a single number from those 6 numbers would get a total different result with other numbers. – Mario Oct 05 '17 at 13:39
  • 1
    Well, if your underlying problem is non linear, you're going to need an entirely different approach. – BurnsBA Oct 05 '17 at 13:42
  • It's not very linear, because by changing some one number it gives better results with other numbers changed. – Mario Oct 05 '17 at 13:51
  • There are any number of ways to improve the search algorithm, but it really depends on the problem, and none of the details of the problem have been given. You could modify multiple parameters in the above example in each loop, or only vary them by, say, 10%, or by changing them any other way that makes sense. But again, it depends on `Calculation` and no details for that have been posted. – BurnsBA Oct 05 '17 at 13:56
  • The calculation is a complex simulation – Mario Oct 05 '17 at 14:02
  • @MarioM Please do some basic research into optimization first as it seems you are lacking a bit. First: my theory-proven statement above still holds: without assumptions brute-force is asymptotically optimal. Second: complex-simulation might effect in indeterministic/noisy behaviour; slow function-evaluation (can't infer gradients) and non-smoothness. Each of these characteristics can be deadly given some approach. So do some research to see what optimization is about and then think about how to present your problem! As you already saw: the answer above already used incompatible assumptions. – sascha Oct 05 '17 at 14:09
  • The function is optimized, actually it does 50 million combinations with brute force in 30minutes (with lower precision numbers), but there are way too many combinations left – Mario Oct 05 '17 at 14:34
  • by changing one parameter it affects the other parameters – Mario Oct 06 '17 at 11:16