|
|
- # Live Simulation
-
- <customHTML />
-
- # Background and Theory
-
- Since you stumbled upon this article, you might be wondering what the heck genetic algorithms are.
- To put it simply: genetic algorithms employ the same tactics used in natural selection to find an optimal solution to an optimization problem.
- Genetic algorithms are often used in high dimensional problems where the optimal solutions are not apparent.
- Genetic algorithms are commonly used to tune the [hyper-parameters](https://en.wikipedia.org/wiki/Hyperparameter) of a program.
- However, this algorithm can be used in any scenario where you have a function which defines how well a solution is.
- Many people have used genetic algorithms in video games to auto learn the weaknesses of players.
-
- The beautiful part about Genetic Algorithms are their simplicity; you need absolutely no knowledge of linear algebra or calculus.
- To implement a genetic algorithm from scratch you only need **very basic** algebra and a general grasp of evolution.
-
- # Genetic Algorithm
-
- All genetic algorithms typically have a single cycle where you continuously mutate, breed, and select the most optimal solutions.
- I will dive into each section of this algorithm using simple JavaScript code snippets.
- The algorithm which I present is very generic and modular so it should be easy to port into other programming languages and applications.
-
- ![Genetic Algorithms Flow Chart](media/GA/GAFlowChart.svg)
-
-
- ## Population Creation
-
- The very first thing we need to do is specify a data-structure for storing our genetic information.
- In biology, chromosomes are composed of sequences of genes.
- Many people run genetic algorithms on binary arrays since they more closely represent DNA.
- However, as computer scientists, it is often easier to model problems using continuous numbers.
- In this approach, every gene will be a single floating point number ranging between zero and one.
- Every type of gene will have a max and min value which represents the absolute extremes of that gene.
- This works well for optimization because it allows us to easily limit our search space.
- For example, we can specify that "height" gene can only vary between 0 and 90.
- To get the actual value of the gene from its \[0-1] value we simple de-normalize it.
-
- $$
- g_{real value} = (g_{high}- g_{low})g_{norm} + g_{low}
- $$
-
- ```javascript
- class Gene
- {
- /**
- * Constructs a new Gene to store in a chromosome.
- * @param min minimum value that this gene can store
- * @param max value this gene can possibly be
- * @param value normalized value
- */
- constructor(min, max, value)
- {
- this.min = min;
- this.max = max;
- this.value = value;
- }
-
- /**
- * De-normalizes the value of the gene
- * @returns {*}
- */
- getRealValue()
- {
- return (this.max - this.min) * this.value + this.min;
- }
-
- getValue()
- {
- return this.value;
- }
-
- setValue(val)
- {
- this.value = val;
- }
-
- makeClone()
- {
- return new Gene(this.min, this.max, this.value);
- }
-
- makeRandomGene()
- {
- return new Gene(this.min, this.max, Math.random());
- }
- }
- ```
-
-
- Now that we have genes, we can create chromosomes.
- Chromosomes are simply collections of genes.
- Whatever language you make this in, make sure that when you create a new chromosome it
- is has a [deep copy](https://en.wikipedia.org/wiki/Object_copying) of the original genetic information rather than a shallow copy.
- A shallow copy is when you simple copy the object pointer where a deep copy is actually creating a new object.
- If you fail to do a deep copy, you will have weird issues where multiple chromosomes will share the same DNA.
-
- In this class I added helper functions to clone the chromosome as a random copy.
- You can only create a new chromosome by cloning because I wanted to keep the program generic and make no assumptions about the domain.
- Since you only provide the min/max information for the genes once, cloning an existing chromosome is the easiest way of
- ensuring that all corresponding chromosomes contain genes with identical extrema.
-
-
- ```javascript
- class Chromosome
- {
- /**
- * Constructs a chromosome by making a copy of
- * a list of genes.
- * @param geneArray
- */
- constructor(geneArray)
- {
- this.genes = [];
- for(let i = 0; i < geneArray.length; i++)
- {
- this.genes.push(geneArray[i].makeClone());
- }
- }
-
- getGenes()
- {
- return this.genes;
- }
-
- /**
- * Mutates a random gene.
- */
- mutate()
- {
- this.genes[Math.round(Math.random() * (this.genes.length-1))].setValue(Math.random());
- }
-
- /**
- * Creates a totally new chromosome with same
- * genetic structure as this chromosome but different
- * values.
- * @returns {Chromosome}
- */
- createRandomChromosome()
- {
- let geneAr = [];
- for(let i = 0; i < this.genes.length; i++)
- {
- geneAr.push(this.genes[i].makeRandomGene());
- }
- return new Chromosome(geneAr);
- }
- }
- ```
-
- Creating a random population is pretty straight forward if implemented a method to create a random clone of a chromosome.
-
- ```javascript
- /**
- * Creates a totally random population based on a desired size
- * and a prototypical chromosome.
- *
- * @param geneticChromosome
- * @param populationSize
- * @returns {Array}
- */
- const createRandomPopulation = function(geneticChromosome, populationSize)
- {
- let population = [];
- for(let i = 0; i < populationSize; i++)
- {
- population.push(geneticChromosome.createRandomChromosome());
- }
- return population;
- };
- ```
-
- This is where nearly all the domain information is introduced.
- After you define what types of genes are found on each chromosome, you can create an entire population.
- In this example all genes contain values ranging between one and ten.
-
- ```javascript
- let gene1 = new Gene(1,10,10);
- let gene2 = new Gene(1,10,0.4);
- let geneList = [gene1, gene2];
-
- let exampleOrganism = new Chromosome(geneList);
-
- let population = createRandomPopulation(genericChromosome, 100);
- ```
-
-
- ## Evaluate Fitness
-
- Like all optimization problems, you need a way to evaluate the performance of a particular solution.
- The cost function takes in a chromosome and evaluates how close it got to the ideal solution.
- This particular example it is just computing the [Manhattan Distance](https://en.wiktionary.org/wiki/Manhattan_distance) to a random 2D point.
- I chose two dimensions because it is easy to graph, however, real applications may have dozens of genes on each chromosome.
-
- ```javascript
- let costx = Math.random() * 10;
- let costy = Math.random() * 10;
-
- /** Defines the cost as the "distance" to a 2-d point.
- * @param chromosome
- * @returns {number}
- */
- const basicCostFunction = function(chromosome)
- {
- return Math.abs(chromosome.getGenes()[0].getRealValue() - costx) +
- Math.abs(chromosome.getGenes()[1].getRealValue() - costy);
- };
- ```
-
- ## Selection
-
- Selecting the best performing chromosomes is straightforward after you have a function for evaluating the performance.
- This code snippet also computes the average and best chromosome of the population to make it easier to graph and define
- the stopping point for the algorithm's main loop.
-
- ```javascript
- /**
- * Function which computes the fitness of everyone in the
- * population and returns the most fit survivors. Method
- * known as elitism.
- *
- * @param population
- * @param keepNumber
- * @param fitnessFunction
- * @returns {{average: number,
- * survivors: Array, bestFit: Chromosome }}
- */
- const naturalSelection = function(population, keepNumber, fitnessFunction)
- {
- let fitnessArray = [];
- let total = 0;
- for(let i = 0; i < population.length; i++)
- {
- const fitness = fitnessFunction(population[i]);
- fitnessArray.push({fit:fitness, chrom: population[i]});
- total+= fitness;
- }
-
- fitnessArray.sort(predicateBy("fit"));
-
- let survivors = [];
- let bestFitness = fitnessArray[0].fit;
- let bestChromosome = fitnessArray[0].chrom;
- for(let i = 0; i < keepNumber; i++)
- {
- survivors.push(fitnessArray[i].chrom);
- }
- return {average: total/population.length, survivors: survivors, bestFit: bestFitness, bestChrom: bestChromosome};
- };
- ```
-
- You might be wondering how I sorted the list of JSON objects - not a numerical array.
- I used the following function as a comparator for JavaScript's built in sort function.
- This comparator will compare objects based on a specific attribute that you give it.
- This is a very handy function to include in all of your JavaScript projects for easy sorting.
-
- ```javascript
- /**
- * Helper function to sort an array
- *
- * @param prop name of JSON property to sort by
- * @returns {function(*, *): number}
- */
- function predicateBy(prop)
- {
- return function(a,b)
- {
- var result;
- if(a[prop] > b[prop])
- {
- result = 1;
- }
- else if(a[prop] < b[prop])
- {
- result = -1;
- }
- return result;
- }
- }
- ```
-
- ## Reproduction
-
- The process of reproduction can be broken down into Pairing and Mating.
-
- ### Pairing
-
- Pairing is the process of selecting mates to produce offspring.
- A typical approach will separate the population into two segments of mothers and fathers.
- You then randomly pick pairs of mothers and fathers to produce offspring.
- It is ok if one chromosome mates more than once.
- It is just important that you keep this process random.
-
- ```javascript
- /**
- * Randomly everyone in the population
- *
- * @param population
- * @param desiredPopulationSize
- */
- const matePopulation = function(population, desiredPopulationSize)
- {
- const originalLength = population.length;
- while(population.length < desiredPopulationSize)
- {
- let index1 = Math.round(Math.random() * (originalLength-1));
- let index2 = Math.round(Math.random() * (originalLength-1));
- if(index1 !== index2)
- {
- const babies = breed(population[index1], population[index2]);
- population.push(babies[0]);
- population.push(babies[1]);
- }
- }
- };
- ```
-
- ### Mating
-
- Mating is the actual act of forming new chromosomes/organisms based on your previously selected pairs.
- From my research, there are two major forms of mating: blending, crossover.
-
- Blending is typically the most preferred approach to mating when dealing with continuous variables.
- In this approach you combine the genes of both parents based on a random factor.
-
- $$
- c_{new} = r * c_{mother} + (1-r) * c_{father}
- $$
-
- The second offspring simply uses (1-r) for their random factor to adjust the chromosomes.
-
- Crossover is the simplest approach to mating.
- In this process you clone the parents and then you randomly swap *n* of their genes.
- This works fine in some scenarios; however, this severely lacks the genetic diversity of the genes because you now have to solely
- rely on mutations for changes.
-
- ```javascript
- /**
- * Mates two chromosomes using the blending method
- * and returns a list of 2 offspring.
- * @param father
- * @param mother
- * @returns {Chromosome[]}
- */
- const breed = function(father, mother)
- {
- let son = new Chromosome(father.getGenes());
- let daughter = new Chromosome(mother.getGenes());
-
- for(let i = 0;i < son.getGenes().length; i++)
- {
- let blendCoef = Math.random();
- blendGene(son.getGenes()[i], daughter.getGenes()[i], blendCoef);
- }
- return [son, daughter];
- };
-
- /**
- * Blends two genes together based on a random blend
- * coefficient.
- **/
- const blendGene = function(gene1, gene2, blendCoef)
- {
- let value1 = (blendCoef * gene1.getValue()) +
- (gene2.getValue() * (1- blendCoef));
- let value2 = ((1-blendCoef) * gene1.getValue()) +
- (gene2.getValue() * blendCoef);
-
- gene1.setValue(value1);
- gene2.setValue(value2);
- };
- ```
-
- ## Mutation
-
- Mutations are random changes to an organisms DNA.
- In the scope of genetic algorithms, it helps our population converge on the correct solution.
-
- You can either adjust genes by a factor resulting in a smaller change or, you can
- change the value of the gene to be something completely random.
- Since we are using the blending technique for reproduction, we already have small incremental changes.
- I prefer to use mutations to randomly change the entire gene since it helps prevent the algorithm
- from settling on a local minimum rather than the global minimum.
-
-
- ```javascript
- /**
- * Randomly mutates the population
- **/
- const mutatePopulation = function(population, mutatePercentage)
- {
- if(population.length >= 2)
- {
- let mutations = mutatePercentage *
- population.length *
- population[0].getGenes().length;
- for(let i = 0; i < mutations; i++)
- {
- population[i].mutate();
- }
- }
- else
- {
- console.log("Error, population too small to mutate");
- }
- };
- ```
-
- ## Immigration
-
- Immigration or "new blood" is the process of dumping random organisms into your population at each generation.
- This prevents us from getting stuck in a local minimum rather than the global minimum.
- There are more advanced techniques to accomplish this same concept.
- My favorite approach (not implemented here) is raising **x** populations simultaneously and every **y** generations
- you take **z** organisms from each population and move them to another population.
-
- ```javascript
- /**
- * Introduces x random chromosomes to the population.
- * @param population
- * @param immigrationSize
- */
- const newBlood = function(population, immigrationSize)
- {
- for(let i = 0; i < immigrationSize; i++)
- {
- let geneticChromosome = population[0];
- population.push(geneticChromosome.createRandomChromosome());
- }
- };
- ```
-
- ## Putting It All Together
-
- Now that we have all the ingredients for a genetic algorithm we can piece it together in a simple loop.
-
- ```javascript
- /**
- * Runs the genetic algorithm by going through the processes of
- * natural selection, mutation, mating, and immigrations. This
- * process will continue until an adequately performing chromosome
- * is found or a generation threshold is passed.
- *
- * @param geneticChromosome Prototypical chromosome: used so algo knows
- * what the dna of the population looks like.
- * @param costFunction Function which defines how bad a Chromosome is
- * @param populationSize Desired population size for population
- * @param maxGenerations Cut off level for number of generations to run
- * @param desiredCost Sufficient cost to terminate program at
- * @param mutationRate Number between [0,1] representing proportion of genes
- * to mutate each generation
- * @param keepNumber Number of Organisms which survive each generation
- * @param newBloodNumber Number of random immigrants to introduce into
- * the population each generation.
- * @returns {*}
- */
- const runGeneticOptimization = function(geneticChromosome, costFunction,
- populationSize, maxGenerations,
- desiredCost, mutationRate, keepNumber,
- newBloodNumber)
- {
- let population = createRandomPopulation(geneticChromosome, populationSize);
- let generation = 0;
- let bestCost = Number.MAX_VALUE;
- let bestChromosome = geneticChromosome;
- do
- {
- matePopulation(population, populationSize);
- newBlood(population, newBloodNumber);
- mutatePopulation(population, mutationRate);
- let generationResult = naturalSelection(population, keepNumber, costFunction);
-
- if(bestCost > generationResult.bestFit)
- {
- bestChromosome = generationResult.bestChrom;
- bestCost = generationResult.bestFit;
- }
- population = generationResult.survivors;
-
- generation++;
- console.log("Generation " + generation + " Best Cost: " + bestCost);
- }while(generation < maxGenerations && bestCost > desiredCost);
- return bestChromosome;
- };
- ```
-
-
- ## Running
-
- Running the program is pretty straight forward after you have your genes and cost function defined.
- You might be wondering if there is an optimal configuration of parameters to use with this algorithm.
- The answer is that it varies based on the particular problem.
- Problems like the one graphed by this website perform very well with a low mutation rate and a high population.
- However, some higher dimensional problems won't even converge on a local answer if you set your mutation rate too low.
-
- ```javascript
- let gene1 = new Gene(1,10,10);
- ...
- let geneN = new Gene(1,10,0.4);
- let geneList = [gene1,..., geneN];
-
- let exampleOrganism = new Chromosome(geneList);
-
- costFunction = function(chromosome)
- {
- var d =...;
- //compute cost
- return d;
- }
-
- runGeneticOptimization(exampleOrganism, costFunction, 100, 50, 0.01, 0.3, 20, 10);
- ```
-
- The complete code for the genetic algorithm and the fancy JavaScript graphs can be found in my [Random Scripts GitHub Repository](https://github.com/jrtechs/RandomScripts).
- In the future I may package this into an [npm](https://www.npmjs.com/) package.
|