|
|
- # 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.
|